import AssertionError from "../error/AssertionError"; import EtudeError from "../error/EtudeError"; import Exceptional from "../util/Exceptional"; import ImmutablePrioritySet from "../util/ImmutablePrioritySet"; import MathUtil from "../util/MathUtil"; import Accidental from "./Accidental"; import Degree from "./Degree"; import Key from "./Key"; import Letter from "./Letter"; import MusicConstants from "./MusicConstants"; import Pitch from "./Pitch"; import Policy from "./Policy"; import Scale from "./Scale"; class KeySignature { private static RELATIVE_MAJOR_TO_MINOR_DISTANCE = -3; public static ORDER_OF_FLATS: Letter[] = "BEADGCF".split("").map(Letter.fromChar).map(e => e.get()); public static ORDER_OF_SHARPS: Letter[] = "FCGDAEB".split("").map(Letter.fromChar).map(e => e.get()); constructor(private accidentals: Key[]) { if (accidentals .map(k => k.getAccidental()) .some(a => a.map(a => a.getOffset()).orElse(0) == 0) ) { throw EtudeError.forInvalid(KeySignature, accidentals, "null or Accidental.NATURAL"); } else if (accidentals.length != new Set(accidentals.map(k => k.getLetter())).size) { throw EtudeError.forInvalid(KeySignature, accidentals, "all letters should be distinct"); } } public getAccidentalCount(): number { return this.accidentals.length; } public getAccidentalFor(letter: Letter): Exceptional { const value: Exceptional = this.accidentals .filter(k => k.getLetter() === letter) .map(k => k.getAccidental()) [0]; return value == null ? Exceptional.empty() : value; } public static getOrderOfFlats(count: number = Letter.size): Letter[] { return KeySignature.ORDER_OF_FLATS.slice(0, count); } public static getOrderOfSharps(count: number = Letter.size): Letter[] { return KeySignature.ORDER_OF_SHARPS.slice(0, count); } public static getKeyFor(accidental: Accidental, count: number, quality: KeySignature.Quality): Exceptional { if (count < 0 || count > Letter.size) { return Exceptional.empty(EtudeError.forInvalid(KeySignature, count, "out of range")); } else if ((count === 0) != (accidental == null || accidental == Accidental.NATURAL)) { return Exceptional.empty(EtudeError.forIllegalArgument(KeySignature, accidental, "key signature with 0 accidentals should have null or Accidental.NATURAL")); } let key: Key; let letter: Letter; // determine the key assuming quality is MAJOR switch (accidental == null ? Accidental.NATURAL : accidental) { case Accidental.FLAT: letter = KeySignature.ORDER_OF_FLATS[MathUtil.floorMod(count - 2, Letter.size)]; key = new Key( letter, // accidental; if flats for key signature contain the letter, make the key flat KeySignature.getOrderOfFlats(count).some(l => l === letter) ? Accidental.FLAT : null ); break; case Accidental.SHARP: letter = KeySignature.ORDER_OF_SHARPS[MathUtil.floorMod(count + 1, Letter.size)]; key = new Key( letter, // accidental; if sharps for key signature contain the letter, make the key sharp KeySignature.getOrderOfSharps(count).some(l => l === letter) ? Accidental.SHARP : null ); break; case Accidental.NATURAL: letter = Letter.C; key = new Key(letter); break; default: return Exceptional.empty(EtudeError.forIllegalArgument(KeySignature, accidental, "should use null, Accidental.NATURAL, Accidental.FLAT, or Accidental.SHARP for key signatures")); } if (quality === KeySignature.Quality.MINOR) { const policies: ImmutablePrioritySet = count === 0 ? Policy.prioritize(Policy.NONE_OR_NATURAL) : Policy.prioritize( Policy.NONE_OR_NATURAL, accidental == Accidental.FLAT ? Policy.FLAT : Policy.SHARP ); const optionalKey: Exceptional = key.step(KeySignature.RELATIVE_MAJOR_TO_MINOR_DISTANCE, policies); if (!optionalKey.isPresent()) { throw new AssertionError("Failed to get relative minor key"); } key = optionalKey.get(); } return Exceptional.of(key); } public static fromAccidentals(accidental: Accidental, count: number): Exceptional { if (count < 0 || count > Letter.size) { return Exceptional.empty(EtudeError.forInvalid(KeySignature, count, "out of range")); } else if ((count === 0) != (accidental == null || accidental == Accidental.NATURAL)) { return Exceptional.empty(EtudeError.forIllegalArgument(KeySignature, accidental, "key signature with 0 accidentals should have null or Accidental.NATURAL")); } let letters: Letter[]; switch (accidental == null ? Accidental.NATURAL : accidental) { case Accidental.FLAT: letters = KeySignature.getOrderOfFlats(count); break; case Accidental.SHARP: letters = KeySignature.getOrderOfSharps(count); break; case Accidental.NATURAL: letters = new Letter[0]; break; default: return Exceptional.empty(EtudeError.forIllegalArgument(KeySignature, accidental, "should use null, Accidental.NATURAL, Accidental.FLAT, or Accidental.SHARP for key signatures")); } const accidentals: Key[] = letters .map(l => new Key(l, accidental)); return Exceptional.of(new KeySignature(accidentals)); } public static fromKey(key: Key, quality: KeySignature.Quality): KeySignature { // arbitrary octave const accidentals: Key[] = new Scale(new Pitch(key, 4), quality === KeySignature.Quality.MAJOR ? Scale.Quality.MAJOR : Scale.Quality.NATURAL_MINOR) .stream() .limit(Letter.size) .map(p => p.getKey()) .filter(k => k.hasAccidental() && !k.isNatural()); if (accidentals.length !== 0) { accidentals[0] .getAccidental() .filter(a => a === Accidental.FLAT || a === Accidental.SHARP) .map(a => { switch (a) { case Accidental.FLAT: return KeySignature.ORDER_OF_FLATS; case Accidental.SHARP: return KeySignature.ORDER_OF_SHARPS; default: throw new AssertionError("Filter failed."); } }) .ifPresent(ordered => accidentals.sort((a, b) => MathUtil.compare(ordered.indexOf(a.getLetter()), ordered.indexOf(b.getLetter()))) ); } return new KeySignature(accidentals); } public toString(): string { return this.accidentals.toString(); } public getAccidentals(): Key[] { return this.accidentals; } } module KeySignature { export class Quality { public static size: number = 0; private static _values: Quality[] = []; public static MAJOR = new Quality(); public static MINOR = new Quality(); private constructor() { ++Quality.size; Quality._values.push(this); } public static values(): Quality[] { return Quality._values.slice(); } public ordinal(): number { return Quality._values.indexOf(this); } public static valueOf(qualityString: string): Quality { const quality: Quality = Quality[qualityString]; if (quality instanceof Quality) { return quality; } throw EtudeError.forInvalid(Quality, qualityString); } public toString(): string { return Object.keys(Quality).find(q => Quality[q] === this); } } } export default KeySignature;