import AssertionError from "../error/AssertionError"; import EtudeError from "../error/EtudeError"; import CircularIterator from "../util/CircularIterator"; import ImmutablePrioritySet from "../util/ImmutablePrioritySet"; import InfiniteIterator from "../util/InfiniteIterator"; import MathUtil from "../util/MathUtil"; import Stream from "../util/Stream"; import StreamUtil from "../util/StreamUtil"; import Direction from "./Direction"; import Letter from "./Letter"; import MusicConstants from "./MusicConstants"; import Pitch from "./Pitch"; import Policy from "./Policy"; class Scale { private quality: Scale.Quality; public constructor(private pitch: Pitch, parameter: number[] | Scale.Quality, descending?: number[]) { if (parameter instanceof Scale.Quality) { this.quality = parameter as Scale.Quality; } else { const ascending = parameter as number[]; this.quality = descending === undefined ? new Scale.Quality(ascending) : new Scale.Quality(ascending, descending); } } public getDefaultPolicy(direction: Direction): ImmutablePrioritySet { if (this.quality.isOctaveRepeating(direction) && this.quality.getStepPattern(direction).length === Letter.size) { // prioritize unique letters const it = Letter.iterator(direction, this.pitch.getKey().getLetter()); let current = it.next().value; return Policy.prioritize(k => { const pass = k.getLetter() === current; // each time the current key finds a match, go to the next letter if (pass) { current = it.next().value; } return pass; }); } else { return Policy.DEFAULT_PRIORITY; } } public stream(direction: Direction | ImmutablePrioritySet = Direction.DEFAULT, policies?: ImmutablePrioritySet): Stream { if (direction instanceof ImmutablePrioritySet) { if (arguments.length !== 1) { throw EtudeError.forIllegalArgument(Scale, arguments, "Invalid number of arguments"); } policies = direction as ImmutablePrioritySet; direction = Direction.DEFAULT; } else if (direction instanceof Direction) { if (policies == null) { policies = this.getDefaultPolicy(direction); } } return StreamUtil.fromIterator(this.iterator(direction, policies)); } public iterator(direction: Direction | ImmutablePrioritySet = Direction.DEFAULT, policies?: ImmutablePrioritySet): Iterator { if (direction instanceof ImmutablePrioritySet) { if (arguments.length !== 1) { throw EtudeError.forIllegalArgument(Scale, arguments, "Invalid number of arguments"); } policies = direction as ImmutablePrioritySet; direction = Direction.DEFAULT; } else if (direction instanceof Direction) { if (policies == null) { policies = this.getDefaultPolicy(direction); } } const it = CircularIterator.of(this.quality.getStepPattern(direction)); return InfiniteIterator.of( Pitch.fromProgramNumber(this.pitch.getProgramNumber(), policies).get(), previous => previous.step(it.next().value, policies).get() ); } public getPitches(direction: Direction | ImmutablePrioritySet = Direction.DEFAULT, policies?: ImmutablePrioritySet): Pitch[] { if (direction instanceof ImmutablePrioritySet) { if (arguments.length !== 1) { throw EtudeError.forIllegalArgument(Scale, arguments, "Invalid number of arguments"); } policies = direction as ImmutablePrioritySet; direction = Direction.DEFAULT; } else if (direction instanceof Direction) { if (policies == null) { policies = this.getDefaultPolicy(direction); } } return this.stream(direction, policies).limit(this.quality.getStepPattern(direction).length); } public toString(direction: Direction = Direction.DEFAULT): string { return "[" + this.getPitches(direction).join(", ") + "]"; } public getPitch(): Pitch { return this.pitch; } public getQuality(): Scale.Quality { return this.quality; } } module Scale { export class Quality { public static MAJOR = new Quality([2, 2, 1, 2, 2, 2, 1]); public static NATURAL_MINOR = new Quality([2, 1, 2, 2, 1, 2, 2]); public static HARMONIC_MINOR = new Quality([2, 1, 2, 2, 1, 3, 1]); public static MELODIC_MINOR = new Quality([2, 1, 2, 2, 2, 2, 1], [-2, -2, -1, -2, -2, -1, -2]); public static CHROMATIC = new Quality([1]); public static WHOLE_TONE = new Quality([2]); // descending: reverse ascending and negate all values (e.g. [1, 3, 5] -> [-5, -3, -1]) constructor(private ascending: number[] = [], private descending: number[] = ascending.slice().reverse().map(n => -n)) { if (ascending.length === 0) { throw EtudeError.forIllegalArgument(Quality, ascending, "step pattern array should not be empty"); } } public getStepPattern(direction: Direction = Direction.DEFAULT): number[] { switch (direction) { case Direction.ASCENDING: return this.ascending.slice(); case Direction.DESCENDING: return this.descending.slice(); default: throw new AssertionError("Invalid direction: " + direction); } } public isOctaveRepeating(direction: Direction = Direction.DEFAULT): boolean { return Math.abs(this.getStepPattern(direction).reduce(MathUtil.add, 0)) === MusicConstants.KEYS_IN_OCTAVE; } } } export default Scale;