import AssertionError from "../error/AssertionError"; import EtudeError from "../error/EtudeError"; import CircularIterator from "../util/CircularIterator"; import Exceptional from "../util/Exceptional"; import EtudeParser from "../util/EtudeParser"; import InfiniteIterator from "../util/InfiniteIterator"; import InfiniteStream from "../util/InfiniteStream"; import Stream from "../util/Stream"; import StreamUtil from "../util/StreamUtil"; import Degree from "./Degree"; import Direction from "./Direction"; import Interval from "./Interval"; import Key from "./Key"; import KeySignature from "./KeySignature"; import Letter from "./Letter"; import Pitch from "./Pitch"; import Scale from "./Scale"; class Chord { private pitches: Pitch[]; public constructor(parameter: Pitch | Pitch[], quality?: Chord.Quality, inversion: Chord.Inversion = Chord.Inversion.ROOT) { if (parameter instanceof Pitch) { if (arguments.length < 2 || arguments.length > 3) { throw new Error("Invalid number of arguments for chord constructor"); } this.pitches = Chord .builder() .setRoot(parameter as Pitch) .add(quality) .setInversion(inversion) .build() .pitches; } else if (parameter instanceof Array) { this.pitches = parameter as Pitch[]; } } public static fromString(chordString: string): Exceptional { return EtudeParser .of(chordString) .filter(c => c != null, EtudeError.forNull(Chord)) .parse(s => { if (!s.includes("[") || !s.includes("]")) { return Exceptional.empty(EtudeError.forInvalid(Chord, s, "missing brackets that enclose pitches")); } if (!s.startsWith("[") || !s.endsWith("]")) { return Exceptional.empty(EtudeError.forInvalid(Chord, s, "contains extra information")); } const pitchesString: string = s.substring(1, s.length - 1); if (pitchesString.includes("[") || pitchesString.includes("]")) { return Exceptional.empty(EtudeError.forInvalid(Chord, s, "contains extra brackets")); } // Pitch[] pitches = Arrays // .stream(pitchesString.split(",")) // .map(Pitch::fromString) // .map(Exceptional::isPresent) // .map(Exceptional::get) // .toArray(Pitch[]::new); // traditional for loop because can't do generic array creation (Exceptional[]) const pitchStrings: string[] = pitchesString.split(","); const pitches: Pitch[] = new Pitch[pitchStrings.length]; for (let i = 0; i < pitchStrings.length; ++i) { const pitch: Exceptional = Pitch.fromString(pitchStrings[i]); if (!pitch.isPresent()) { return Exceptional.empty(EtudeError.forInvalid(Chord, s, pitch.getException().message)); } pitches[i] = pitch.get(); } return Exceptional.of(pitches); }) .get(a => new Chord(a[0] as (Pitch[]))); } public toString(): string { return "[" + this.pitches.join(", ") + "]"; } public stream(direction: Direction = Direction.DEFAULT): Stream { return StreamUtil.fromIterator(this.iterator(direction)); } public iterator(direction: Direction = Direction.DEFAULT): Iterator { const keys: Key[] = this.pitches.map(p => p.getKey()); const it: CircularIterator = direction == Direction.ASCENDING ? CircularIterator.of(keys) : CircularIterator.of(keys).reverse(); // skip first key because equal to initial pitch's (pitches[0]) key it.next(); return InfiniteIterator.of( this.pitches[0], direction == Direction.ASCENDING ? previous => previous.getHigherPitch(it.next().value).get() : previous => previous.getLowerPitch(it.next().value).get() ); } public getPitches(): Pitch[] { return this.pitches.slice(); } public static builder(): Chord.RequiresRoot { return new Chord.Builder(); } } module Chord { export class Inversion { public static size = 0; private static _values = []; public static ROOT = new Inversion(); public static FIRST = new Inversion(); public static SECOND = new Inversion(); public static THIRD = new Inversion(); private constructor() { ++Inversion.size; Inversion._values.push(this); } public static values(): Inversion[] { return Inversion._values.slice(); } public ordinal(): number { return Inversion._values.indexOf(this); } public static valueOf(inversionString: string): Inversion { let inversion = Inversion[inversionString]; if (inversion instanceof Inversion) { return inversion; } throw EtudeError.forInvalid(Inversion, inversionString); } public getValue(): number { return this.ordinal(); } public toString(): string { return Object.keys(Inversion).find(i => Inversion[i] === this); } } export class Quality { public static MAJOR = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MAJOR, 3), new Interval(Interval.Quality.PERFECT, 5) ]); public static MINOR = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MINOR, 3), new Interval(Interval.Quality.PERFECT, 5) ]); public static DIMINISHED = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MINOR, 3), new Interval(Interval.Quality.DIMINISHED, 5) ]); public static AUGMENTED = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MAJOR, 3), new Interval(Interval.Quality.AUGMENTED, 5) ]); public static MAJOR_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MAJOR, 3), new Interval(Interval.Quality.PERFECT, 5), new Interval(Interval.Quality.MAJOR, 7) ]); public static MINOR_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MINOR, 3), new Interval(Interval.Quality.PERFECT, 5), new Interval(Interval.Quality.MINOR, 7) ]); public static DOMINANT_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MAJOR, 3), new Interval(Interval.Quality.PERFECT, 5), new Interval(Interval.Quality.MINOR, 7) ]); public static DIMINISHED_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MINOR, 3), new Interval(Interval.Quality.DIMINISHED, 5), new Interval(Interval.Quality.DIMINISHED, 7) ]); public static HALF_DIMINISHED_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MINOR, 3), new Interval(Interval.Quality.DIMINISHED, 5), new Interval(Interval.Quality.MINOR, 7) ]); public static MINOR_MAJOR_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MINOR, 3), new Interval(Interval.Quality.PERFECT, 5), new Interval(Interval.Quality.MAJOR, 7) ]); public static AUGMENTED_MAJOR_SEVENTH = new Quality([ new Interval(Interval.Quality.PERFECT, 1), new Interval(Interval.Quality.MAJOR, 3), new Interval(Interval.Quality.AUGMENTED, 5), new Interval(Interval.Quality.MAJOR, 7) ]); constructor(private intervalPattern: Interval[]) { } public getIntervalPattern(): Interval[] { return this.intervalPattern.slice(); } } export class Builder implements Base, RequiresRoot, Manipulate, End { public pitches: Pitch[] = []; public bottomDegree = Degree.TONIC; public root: Pitch; public setRoot(root: Pitch): Manipulate { this.root = root; return this; } public add(element: Interval | Quality): Manipulate { if (element instanceof Interval) { this.pitches.push(this.root.step(element).get()); } else if (element instanceof Quality) { element .getIntervalPattern() .forEach(i => this.add(i)); } return this; } public setInversion(inversion: Inversion): End { switch (inversion) { case Inversion.ROOT: return this.setBottomDegree(Degree.TONIC); case Inversion.FIRST: return this.setBottomDegree(Degree.MEDIANT); case Inversion.SECOND: return this.setBottomDegree(Degree.DOMINANT); case Inversion.THIRD: return this.setBottomDegree(Degree.LEADING_TONE); default: throw new AssertionError("Invalid inversion: " + inversion); } } public setBottomDegree(degree: Degree): End { this.bottomDegree = degree; return this; } public build(): Chord { const deque: Pitch[] = this.pitches.slice(); // arbitrary octave const scale: Scale = new Scale(new Pitch(this.root.getKey(), 4), Scale.Quality.MAJOR); const letter: Letter = scale.getPitches()[this.bottomDegree.getValue() - 1].getKey().getLetter(); const value: Pitch = deque .find(p => p.getKey().getLetter() == letter); const exceptional: Exceptional = Exceptional.ofNullable(value); if (!exceptional.isPresent()) { throw EtudeError.forIllegalState(Chord, "Unable to invert chord: missing " + this.bottomDegree + " pitch"); } const lowestPitch: Pitch = exceptional.get(); // guaranteed to terminate because the exceptional above while (lowestPitch != deque[0]) { deque.push(lowestPitch.getHigherPitch(deque.shift().getKey()).get()); } return new Chord(deque); } } export interface Base { build(): Chord; } export interface RequiresRoot { setRoot(root: Pitch): Manipulate; } export interface Manipulate extends Base { add(element: Interval | Quality): Manipulate; setInversion(inversion: Inversion): End; setBottomDegree(degree: Degree): End; } export interface End extends Base { } } export default Chord;