Skip to main content

music21_rs/key/
keysignature.rs

1use crate::{
2    defaults::IntegerType,
3    error::{Error, Result},
4    interval::Interval,
5    pitch::{Accidental, Pitch},
6    scale::{FIFTHS_ORDER_SHARP, Scale, ScaleType},
7};
8
9use super::Key;
10
11use crate::interval::constants::{
12    PERFECT_FIFTH_DOWN, PERFECT_FIFTH_UP, PERFECT_FIFTH_UP as PERFECT_FIFTH, PERFECT_FOURTH_UP,
13    PERFECT_FOURTH_UP as PERFECT_FOURTH,
14};
15
16const MODE_SHARPS_ALTER: [(&str, IntegerType); 9] = [
17    ("major", 0),
18    ("ionian", 0),
19    ("minor", -3),
20    ("aeolian", -3),
21    ("dorian", -2),
22    ("phrygian", -4),
23    ("lydian", 1),
24    ("mixolydian", -1),
25    ("locrian", -5),
26];
27
28fn canonical_mode_for_offset(offset: IntegerType) -> Option<&'static str> {
29    match offset {
30        0 => Some("ionian"),
31        -1 => Some("mixolydian"),
32        -2 => Some("dorian"),
33        -3 => Some("aeolian"),
34        -4 => Some("phrygian"),
35        -5 => Some("locrian"),
36        1 => Some("lydian"),
37        _ => None,
38    }
39}
40
41/// Returns the circle-of-fifths sharp-count offset for a mode name.
42pub fn mode_sharps_alter(mode: &str) -> Option<IntegerType> {
43    MODE_SHARPS_ALTER
44        .iter()
45        .find_map(|(name, value)| (*name == mode.to_lowercase()).then_some(*value))
46}
47
48/// Returns the major-key tonic pitch for a key-signature sharp count.
49pub fn sharps_to_pitch(sharp_count: IntegerType) -> Result<Pitch> {
50    if sharp_count == 0 {
51        return Pitch::from_name("C");
52    }
53
54    let mut pitch = Pitch::from_name("C")?;
55    pitch.octave_setter(None);
56
57    let interval = if sharp_count > 0 {
58        &*PERFECT_FIFTH
59    } else {
60        &*PERFECT_FIFTH_DOWN
61    };
62
63    for _ in 0..sharp_count.abs() {
64        pitch = interval.transpose_pitch(&pitch)?;
65        pitch.octave_setter(None);
66    }
67    Ok(pitch)
68}
69
70/// Returns the key-signature sharp count for a tonic pitch and optional mode.
71pub fn pitch_to_sharps(pitch_value: &Pitch, mode: Option<&str>) -> Result<IntegerType> {
72    let step_index = FIFTHS_ORDER_SHARP
73        .iter()
74        .position(|step| *step == pitch_value.step())
75        .ok_or_else(|| Error::StepName("cannot map step to circle of fifths".to_string()))?;
76
77    let mut sharps = step_index as IntegerType - 1;
78    if !pitch_value.accidental().is_twelve_tone() {
79        return Err(Error::Key(
80            "Cannot determine sharps for quarter-tone keys! silly!".to_string(),
81        ));
82    }
83    sharps += 7 * pitch_value.accidental().alter() as IntegerType;
84
85    // A mode nobody has a signature alteration for leaves the signature
86    // where the major key put it, which is what music21 does: `C
87    // hypomixolydian` is written with no sharps and no flats.
88    if let Some(offset) = mode.and_then(mode_sharps_alter) {
89        sharps += offset;
90    }
91
92    Ok(sharps)
93}
94
95/// Returns the key-signature sharp count for a tonic pitch name and optional mode.
96pub fn pitch_name_to_sharps(pitch_name: &str, mode: Option<&str>) -> Result<IntegerType> {
97    let pitch = Pitch::from_name(pitch_name)?;
98    pitch_to_sharps(&pitch, mode)
99}
100
101#[derive(Clone, Debug)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103/// A key signature represented by the number of sharps.
104///
105/// Flats are represented as negative sharps, so B-flat major has `-2`.
106#[must_use]
107pub struct KeySignature {
108    sharps: Option<IntegerType>,
109    altered: Option<Vec<Pitch>>,
110    accidentals_apply_only_to_octave: bool,
111}
112
113impl KeySignature {
114    /// Creates a key signature from a sharp count.
115    pub fn new(sharps: IntegerType) -> Self {
116        Self {
117            sharps: Some(sharps),
118            altered: None,
119            accidentals_apply_only_to_octave: false,
120        }
121    }
122
123    /// Creates a non-traditional key signature from the pitches it alters,
124    /// as music21 does when `alteredPitches` is assigned: `E-` and `G#`
125    /// together, say, which no count of sharps can express. Such a signature
126    /// has no sharp count and no key.
127    pub fn from_altered_pitches(pitches: Vec<Pitch>) -> Self {
128        Self {
129            sharps: None,
130            altered: Some(pitches),
131            accidentals_apply_only_to_octave: false,
132        }
133    }
134
135    /// Returns the number of sharps, with flats as negative values, or
136    /// `None` for a non-traditional signature.
137    pub fn sharps(&self) -> Option<IntegerType> {
138        self.sharps
139    }
140
141    /// Replaces the sharp count, turning a non-traditional signature back
142    /// into a traditional one.
143    pub fn set_sharps(&mut self, sharps: IntegerType) {
144        self.sharps = Some(sharps);
145        self.altered = None;
146    }
147
148    /// Replaces the altered pitches, turning the signature non-traditional.
149    pub fn set_altered_pitches(&mut self, pitches: Vec<Pitch>) {
150        self.sharps = None;
151        self.altered = Some(pitches);
152    }
153
154    /// Whether the signature is a list of altered pitches rather than a
155    /// count of sharps or flats.
156    ///
157    /// music21's `isNonTraditional`. There `sharps` answers nought when this
158    /// is true; [`Self::sharps`] answers `None`, which says both things at
159    /// once.
160    pub fn is_non_traditional(&self) -> bool {
161        self.sharps.is_none()
162    }
163
164    /// Makes the signature non-traditional, or traditional again: the setter
165    /// of music21's `isNonTraditional`.
166    ///
167    /// Turning it on drops the sharp count and starts an empty list of
168    /// altered pitches; turning it off restores a count of nought. Setting it
169    /// to what it already is does nothing.
170    pub fn set_non_traditional(&mut self, non_traditional: bool) {
171        if non_traditional == self.sharps.is_none() {
172            return;
173        }
174        if non_traditional {
175            self.sharps = None;
176            self.altered = Some(Vec::new());
177        } else {
178            self.sharps = Some(0);
179            self.altered = None;
180        }
181    }
182
183    /// Whether the altered pitches of a non-traditional signature carry
184    /// octaves that limit where they apply, as `F#4` altering only that
185    /// octave. music21 records the flag; applying it is a stream's job.
186    pub fn accidentals_apply_only_to_octave(&self) -> bool {
187        self.accidentals_apply_only_to_octave
188    }
189
190    /// Sets whether the altered pitches apply only in their own octave.
191    pub fn set_accidentals_apply_only_to_octave(&mut self, value: bool) {
192        self.accidentals_apply_only_to_octave = value;
193    }
194
195    fn sharps_or_error(&self) -> Result<IntegerType> {
196        self.sharps.ok_or_else(|| {
197            Error::Key(
198                "a non-traditional key signature has no count of sharps or flats".to_string(),
199            )
200        })
201    }
202
203    /// The description music21 prints after `of`: `3 sharps`, `1 flat`,
204    /// `no sharps or flats`, or `pitches: [E-, G#]`.
205    pub fn description(&self) -> String {
206        match self.sharps {
207            None => {
208                let names: Vec<String> = self
209                    .altered
210                    .as_deref()
211                    .unwrap_or_default()
212                    .iter()
213                    .map(Pitch::to_string)
214                    .collect();
215                format!("pitches: [{}]", names.join(", "))
216            }
217            Some(0) => "no sharps or flats".to_string(),
218            Some(1) => "1 sharp".to_string(),
219            Some(-1) => "1 flat".to_string(),
220            Some(n) if n > 1 => format!("{n} sharps"),
221            Some(n) => format!("{} flats", -n),
222        }
223    }
224
225    /// Returns the pitches this signature alters, in circle-of-fifths order
226    /// and without octaves: `F# C# G#` for three sharps, `B- E- A-` for three
227    /// flats; a non-traditional signature answers the pitches it was given.
228    pub fn altered_pitches(&self) -> Result<Vec<Pitch>> {
229        if let Some(altered) = &self.altered {
230            return Ok(altered.clone());
231        }
232        let sharps = self.sharps_or_error()?;
233        let (start, interval) = if sharps > 0 {
234            ("B", &*PERFECT_FIFTH)
235        } else {
236            ("F", &*PERFECT_FOURTH)
237        };
238        let mut current = Pitch::from_name(start)?;
239        let mut altered = Vec::with_capacity(sharps.unsigned_abs() as usize);
240        for _ in 0..sharps.abs() {
241            current = interval.transpose_pitch(&current)?;
242            current.octave_setter(None);
243            altered.push(current.clone());
244        }
245        Ok(altered)
246    }
247
248    /// Returns the accidental this signature puts on a step letter, if any.
249    pub fn accidental_by_step(&self, step: char) -> Result<Option<Accidental>> {
250        let step = crate::stepname::StepName::try_from(step)?;
251        Ok(self
252            .altered_pitches()?
253            .into_iter()
254            .rev()
255            .find(|pitch| pitch.step() == step)
256            .and_then(|pitch| pitch.explicit_accidental().cloned()))
257    }
258
259    /// Returns the signature of the major key this one's major tonic moves
260    /// to by the interval.
261    pub fn transpose(&self, interval: &Interval) -> Result<Self> {
262        let tonic = self.try_as_key(Some("major"), None)?.tonic();
263        let mut transposed = interval.transpose_pitch(&tonic)?;
264        if interval.implicit_diatonic && pitch_to_sharps(&transposed, None)?.abs() > 6 {
265            transposed = transposed.get_enharmonic()?;
266        }
267        Ok(Self::new(pitch_to_sharps(&transposed, None)?))
268    }
269
270    /// Transposes a pitch spelled in C into this key signature the way
271    /// music21's `transposePitchFromC` does: up a fifth per sharp, or up a
272    /// fourth per flat, keeping the pitch's own octave, so `E4` in two sharps
273    /// is `F#4` and in three flats `G4`.
274    pub fn transpose_pitch_from_c(&self, pitch: &Pitch) -> Result<Pitch> {
275        let sharps = self.sharps_or_error()?;
276        if sharps == 0 {
277            return Ok(pitch.clone());
278        }
279        let step = if sharps < 0 {
280            &*PERFECT_FOURTH_UP
281        } else {
282            &*PERFECT_FIFTH_UP
283        };
284        let mut transposed = pitch.clone();
285        for _ in 0..sharps.unsigned_abs() {
286            transposed = step.transpose_pitch_with_options(&transposed, false, None)?;
287        }
288        transposed.octave_setter(pitch.octave());
289        Ok(transposed)
290    }
291
292    /// Returns the major or minor scale this signature implies.
293    pub fn scale(&self, mode: &str) -> Result<Scale> {
294        let scale_type = match mode {
295            "major" => ScaleType::Major,
296            "minor" => ScaleType::Minor,
297            other => {
298                return Err(Error::Key(format!(
299                    "no mapping to a scale exists for this mode yet: {other}"
300                )));
301            }
302        };
303        Ok(Scale::new(
304            scale_type,
305            self.try_as_key(Some(mode), None)?.tonic(),
306        ))
307    }
308
309    /// Converts this signature to a key in the given mode.
310    pub fn as_key(&self, mode: &str) -> Key {
311        self.try_as_key(Some(mode), None).unwrap_or_else(|_| {
312            Key::new(Pitch::from_name("C").expect("C is valid pitch"), "major", 0)
313        })
314    }
315
316    /// Converts this signature to a key, optionally inferring mode from tonic.
317    pub fn try_as_key(&self, mode: Option<&str>, tonic: Option<&str>) -> Result<Key> {
318        let our_sharps = self.sharps_or_error()?;
319
320        let resolved_mode = if mode.is_none() && tonic.is_none() {
321            "major".to_string()
322        } else if mode.is_none() && tonic.is_some() {
323            let tonic_name = tonic.expect("checked is_some above");
324            let major_sharps = pitch_name_to_sharps(tonic_name, None)?;
325            canonical_mode_for_offset(our_sharps - major_sharps)
326                .ok_or_else(|| {
327                    Error::Key(format!(
328                        "Could not solve for mode from sharps={our_sharps}, tonic={tonic_name}"
329                    ))
330                })?
331                .to_string()
332        } else {
333            mode.expect("checked is_some above").to_lowercase()
334        };
335
336        let sharp_alteration_from_major = mode_sharps_alter(&resolved_mode)
337            .ok_or_else(|| Error::Key(format!("Mode {resolved_mode} is unknown")))?;
338
339        let tonic_pitch = sharps_to_pitch(our_sharps - sharp_alteration_from_major)?;
340        Ok(Key::new(tonic_pitch, &resolved_mode, our_sharps))
341    }
342}
343
344impl std::fmt::Display for KeySignature {
345    /// `KeySignature of 3 sharps`, `KeySignature of pitches: [E-, G#]`.
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        write!(f, "KeySignature of {}", self.description())
348    }
349}
350
351#[cfg(test)]
352mod non_traditional_tests {
353    use super::*;
354
355    /// A non-traditional signature reports no sharp count, and the flag can
356    /// be turned on and off again.
357    #[test]
358    fn a_signature_can_be_made_non_traditional_and_back() {
359        let mut signature = KeySignature::new(3);
360        assert!(!signature.is_non_traditional());
361
362        signature.set_non_traditional(true);
363        assert!(signature.is_non_traditional());
364        assert_eq!(signature.sharps(), None);
365
366        signature.set_non_traditional(false);
367        assert!(!signature.is_non_traditional());
368        assert_eq!(signature.sharps(), Some(0));
369
370        // Setting it to what it already is leaves the count alone.
371        let mut three = KeySignature::new(3);
372        three.set_non_traditional(false);
373        assert_eq!(three.sharps(), Some(3));
374    }
375
376    fn pitches(names: &[&str]) -> Vec<Pitch> {
377        names
378            .iter()
379            .map(|name| Pitch::from_name(*name).unwrap())
380            .collect()
381    }
382
383    #[test]
384    fn a_pitch_list_makes_a_non_traditional_signature() {
385        let unusual = KeySignature::from_altered_pitches(pitches(&["E-", "G#"]));
386        assert!(unusual.is_non_traditional());
387        assert_eq!(unusual.sharps(), None);
388        assert_eq!(unusual.to_string(), "KeySignature of pitches: [E-, G#]");
389        assert_eq!(
390            unusual.accidental_by_step('G').unwrap().unwrap().name(),
391            "sharp"
392        );
393        assert_eq!(unusual.accidental_by_step('A').unwrap(), None);
394        assert!(unusual.try_as_key(Some("major"), None).is_err());
395        assert!(
396            unusual
397                .transpose_pitch_from_c(&Pitch::from_name("C4").unwrap())
398                .is_err()
399        );
400    }
401
402    #[test]
403    fn setting_pitches_and_sharps_switches_kinds() {
404        let mut signature = KeySignature::new(2);
405        assert_eq!(signature.to_string(), "KeySignature of 2 sharps");
406        signature.set_altered_pitches(pitches(&["F#4"]));
407        assert!(signature.is_non_traditional());
408        assert_eq!(signature.to_string(), "KeySignature of pitches: [F#4]");
409        assert!(!signature.accidentals_apply_only_to_octave());
410        signature.set_accidentals_apply_only_to_octave(true);
411        assert!(signature.accidentals_apply_only_to_octave());
412        signature.set_sharps(-1);
413        assert!(!signature.is_non_traditional());
414        assert_eq!(signature.to_string(), "KeySignature of 1 flat");
415        assert_eq!(
416            KeySignature::new(0).to_string(),
417            "KeySignature of no sharps or flats"
418        );
419        assert_eq!(KeySignature::new(-4).description(), "4 flats");
420    }
421
422    #[test]
423    fn traditional_altered_pitches_carry_explicit_accidentals() {
424        let three_sharps = KeySignature::new(3);
425        let altered = three_sharps.altered_pitches().unwrap();
426        assert!(altered.iter().all(Pitch::has_accidental));
427        assert_eq!(
428            three_sharps
429                .accidental_by_step('F')
430                .unwrap()
431                .unwrap()
432                .name(),
433            "sharp"
434        );
435    }
436}
437
438#[cfg(test)]
439mod tests {
440
441    #[test]
442    fn a_signature_names_a_key_in_a_mode_or_from_a_tonic() {
443        let two_sharps = KeySignature::new(2);
444        assert_eq!(two_sharps.as_key("major").tonic().name(), "D");
445        assert_eq!(two_sharps.as_key("minor").tonic().name(), "B");
446        let from_tonic = two_sharps.try_as_key(None, Some("b")).unwrap();
447        assert_eq!(
448            (from_tonic.tonic().name(), from_tonic.mode()),
449            ("B".to_string(), "aeolian")
450        );
451        assert_eq!(
452            two_sharps.try_as_key(None, None).unwrap().tonic().name(),
453            "D"
454        );
455        // A mode given beside a tonic wins, and the tonic is not consulted.
456        assert_eq!(
457            two_sharps
458                .try_as_key(Some("major"), Some("C"))
459                .unwrap()
460                .tonic()
461                .name(),
462            "D"
463        );
464        let altered = KeySignature::from_altered_pitches(vec![Pitch::from_name("F#").unwrap()]);
465        assert!(altered.try_as_key(Some("major"), None).is_err());
466        assert_eq!(altered.as_key("major").tonic().name(), "C");
467        assert_eq!(
468            pitch_to_sharps(&Pitch::from_name("D").unwrap(), Some("major")).unwrap(),
469            2
470        );
471        assert_eq!(
472            pitch_to_sharps(&Pitch::from_name("D").unwrap(), Some("minor")).unwrap(),
473            -1
474        );
475        // A mode with no signature alteration leaves the major key's count.
476        assert_eq!(
477            pitch_to_sharps(&Pitch::from_name("D").unwrap(), Some("nonsense")).unwrap(),
478            2
479        );
480    }
481
482    #[test]
483    fn transpose_pitch_from_c_matches_music21() {
484        let cases: [(i32, [&str; 5]); 7] = [
485            (0, ["C4", "E4", "B-3", "F#5", "G"]),
486            (1, ["G4", "B4", "F3", "C#5", "D"]),
487            (2, ["D4", "F#4", "C3", "G#5", "A"]),
488            (-1, ["F4", "A4", "E-3", "B5", "C"]),
489            (-3, ["E-4", "G4", "D-3", "A5", "B-"]),
490            (7, ["C#4", "E#4", "B3", "F##5", "G#"]),
491            (-7, ["C-4", "E-4", "B--3", "F5", "G-"]),
492        ];
493        for (sharps, expected) in cases {
494            let signature = KeySignature::new(sharps);
495            let actual = ["C4", "E4", "B-3", "F#5", "G"].map(|name| {
496                signature
497                    .transpose_pitch_from_c(&Pitch::from_name(name).unwrap())
498                    .unwrap()
499                    .name_with_octave()
500            });
501            assert_eq!(actual, expected, "{sharps} sharps");
502        }
503    }
504    use super::*;
505
506    #[test]
507    fn altered_pitches_and_transpositions_match_music21() {
508        type Row = (
509            i32,
510            &'static [&'static str],
511            [Option<&'static str>; 3],
512            [i32; 3],
513            &'static str,
514            &'static str,
515        );
516        let cases: [Row; 15] = [
517            (
518                -7,
519                &["B-", "E-", "A-", "D-", "G-", "C-", "F-"],
520                [Some("flat"), Some("flat"), Some("flat")],
521                [-6, -8, -12],
522                "C-",
523                "A-",
524            ),
525            (
526                -6,
527                &["B-", "E-", "A-", "D-", "G-", "C-"],
528                [None, Some("flat"), Some("flat")],
529                [-5, -7, -11],
530                "G-",
531                "E-",
532            ),
533            (
534                -5,
535                &["B-", "E-", "A-", "D-", "G-"],
536                [None, Some("flat"), None],
537                [-4, -6, -10],
538                "D-",
539                "B-",
540            ),
541            (
542                -4,
543                &["B-", "E-", "A-", "D-"],
544                [None, Some("flat"), None],
545                [-3, -5, -9],
546                "A-",
547                "F",
548            ),
549            (
550                -3,
551                &["B-", "E-", "A-"],
552                [None, Some("flat"), None],
553                [-2, -4, -8],
554                "E-",
555                "C",
556            ),
557            (
558                -2,
559                &["B-", "E-"],
560                [None, Some("flat"), None],
561                [-1, -3, -7],
562                "B-",
563                "G",
564            ),
565            (
566                -1,
567                &["B-"],
568                [None, Some("flat"), None],
569                [0, -2, -6],
570                "F",
571                "D",
572            ),
573            (0, &[], [None, None, None], [1, -1, -5], "C", "A"),
574            (
575                1,
576                &["F#"],
577                [Some("sharp"), None, None],
578                [2, 0, -4],
579                "G",
580                "E",
581            ),
582            (
583                2,
584                &["F#", "C#"],
585                [Some("sharp"), None, Some("sharp")],
586                [3, 1, -3],
587                "D",
588                "B",
589            ),
590            (
591                3,
592                &["F#", "C#", "G#"],
593                [Some("sharp"), None, Some("sharp")],
594                [4, 2, -2],
595                "A",
596                "F#",
597            ),
598            (
599                4,
600                &["F#", "C#", "G#", "D#"],
601                [Some("sharp"), None, Some("sharp")],
602                [5, 3, -1],
603                "E",
604                "C#",
605            ),
606            (
607                5,
608                &["F#", "C#", "G#", "D#", "A#"],
609                [Some("sharp"), None, Some("sharp")],
610                [6, 4, 0],
611                "B",
612                "G#",
613            ),
614            (
615                6,
616                &["F#", "C#", "G#", "D#", "A#", "E#"],
617                [Some("sharp"), None, Some("sharp")],
618                [7, 5, 1],
619                "F#",
620                "D#",
621            ),
622            (
623                7,
624                &["F#", "C#", "G#", "D#", "A#", "E#", "B#"],
625                [Some("sharp"), Some("sharp"), Some("sharp")],
626                [8, 6, 2],
627                "C#",
628                "A#",
629            ),
630        ];
631        for (sharps, altered, by_step, transposed, major, minor) in cases {
632            let signature = KeySignature::new(sharps);
633            let names = signature
634                .altered_pitches()
635                .unwrap()
636                .iter()
637                .map(Pitch::name)
638                .collect::<Vec<_>>();
639            assert_eq!(names, altered, "{sharps} altered");
640            for (step, expected) in ['F', 'B', 'C'].into_iter().zip(by_step) {
641                let accidental = signature.accidental_by_step(step).unwrap();
642                assert_eq!(
643                    accidental.as_ref().map(Accidental::name),
644                    expected,
645                    "{sharps} {step}"
646                );
647            }
648            for (name, expected) in ["P5", "-P5", "m2"].into_iter().zip(transposed) {
649                let moved = signature
650                    .transpose(&Interval::from_name(name).unwrap())
651                    .unwrap();
652                assert_eq!(moved.sharps(), Some(expected), "{sharps} by {name}");
653            }
654            assert_eq!(signature.scale("major").unwrap().tonic().name(), major);
655            assert_eq!(signature.scale("minor").unwrap().tonic().name(), minor);
656        }
657        assert!(KeySignature::new(0).scale("dorian").is_err());
658        assert!(KeySignature::new(0).accidental_by_step('H').is_err());
659    }
660    #[test]
661    fn keysignature_as_key_major_minor() {
662        let ks = KeySignature::new(2);
663        assert_eq!(ks.as_key("major").tonic().name(), "D");
664        assert_eq!(ks.as_key("minor").tonic().name(), "B");
665    }
666
667    #[test]
668    fn keysignature_mode_inference_from_tonic() {
669        let ks = KeySignature::new(0);
670        let key = ks.try_as_key(None, Some("D")).unwrap();
671        assert_eq!(key.mode(), "dorian");
672        assert_eq!(key.tonic().name(), "D");
673    }
674
675    #[test]
676    fn sharps_to_pitch_roundtrip() {
677        let f_sharp = sharps_to_pitch(6).unwrap();
678        assert_eq!(f_sharp.name(), "F#");
679        let b_flat = sharps_to_pitch(-2).unwrap();
680        assert_eq!(b_flat.name(), "B-");
681        assert_eq!(sharps_to_pitch(-7).unwrap().name(), "C-");
682        assert_eq!(sharps_to_pitch(7).unwrap().name(), "C#");
683        assert_eq!(KeySignature::new(-7).as_key("major").tonic().name(), "C-");
684    }
685}