pedromsantos/glenn

View on GitHub
src/Domain/Scale.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
import { ScalePatternPrimitives, ScalePrimitives } from '../primitives/Scale';
import { Chord, ChordFunction, ChordPattern, ChordPitch, ChordPitches, ClosedChord } from './Chord';
import { Duration } from './Duration';
import ensure, { throwExpression } from './Ensure';
import { Interval } from './Interval';
import { MelodicLine, Note, Octave } from './Note';
import { Pitch, PitchLine, PitchLineDirection } from './Pitch';

export const enum ScaleDegree {
  I = 0,
  II,
  III,
  IV,
  V,
  VI,
  VII,
}

export class ScalePattern {
  private static readonly all: ScalePattern[] = [];

  private constructor(
    private readonly name: string,
    private readonly pattern: Interval[]
  ) {
    ScalePattern.all.push(this);
  }

  public static readonly Ionian: ScalePattern = new ScalePattern('Ionian', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MajorSeventh,
  ]);

  public static readonly Dorian: ScalePattern = new ScalePattern('Dorian', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MinorSeventh,
  ]);

  public static readonly Phrygian: ScalePattern = new ScalePattern('Phrygian', [
    Interval.Unison,
    Interval.MinorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MinorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly Lydian: ScalePattern = new ScalePattern('Lydian', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.AugmentedFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MajorSeventh,
  ]);
  public static readonly Mixolydian: ScalePattern = new ScalePattern('Mixolydian', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly Aolian: ScalePattern = new ScalePattern('Aolian', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MinorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly Locrian: ScalePattern = new ScalePattern('Locrian', [
    Interval.Unison,
    Interval.MinorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.DiminishedFifth,
    Interval.MinorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly MajorPentatonic: ScalePattern = new ScalePattern('Major Pentatonic', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.PerfectFifth,
    Interval.MajorSixth,
  ]);
  public static readonly MinorPentatonic: ScalePattern = new ScalePattern('Minor Pentatonic', [
    Interval.Unison,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MinorSeventh,
  ]);
  public static readonly Blues: ScalePattern = new ScalePattern('Blues', [
    Interval.Unison,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.DiminishedFifth,
    Interval.PerfectFifth,
    Interval.MinorSeventh,
  ]);
  public static readonly HarmonicMinor: ScalePattern = new ScalePattern('Harmonic Minor', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MinorSixth,
    Interval.MajorSeventh,
  ]);
  public static readonly MelodicMinor: ScalePattern = new ScalePattern('Melodic Minor', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MajorSeventh,
  ]);
  public static readonly Dorianb2: ScalePattern = new ScalePattern('Dorian b2', [
    Interval.Unison,
    Interval.MinorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly NeapolitanMinor: ScalePattern = new ScalePattern('Neapolitan Minor', [
    Interval.Unison,
    Interval.MinorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MinorSixth,
    Interval.MajorSeventh,
  ]);
  public static readonly LydianAugmented: ScalePattern = new ScalePattern('Lydian Augmented', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.AugmentedFourth,
    Interval.AugmentedFifth,
    Interval.MajorSixth,
    Interval.MajorSeventh,
  ]);
  public static readonly LydianDominant: ScalePattern = new ScalePattern('Lydian Dominant', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.AugmentedFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly Mixolydianb6: ScalePattern = new ScalePattern('Mixolydian b6', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MinorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly Bebop: ScalePattern = new ScalePattern('Bebop', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.PerfectFourth,
    Interval.PerfectFifth,
    Interval.MajorSixth,
    Interval.MinorSeventh,
    Interval.MajorSeventh,
  ]);
  public static readonly LocrianSharp2: ScalePattern = new ScalePattern('Locrian #2', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MinorThird,
    Interval.PerfectFourth,
    Interval.DiminishedFifth,
    Interval.MinorSixth,
    Interval.MinorSeventh,
  ]);
  public static readonly AlteredDominant: ScalePattern = new ScalePattern('Altered Dominant', [
    Interval.Unison,
    Interval.MinorSecond,
    Interval.AugmentedSecond,
    Interval.MajorThird,
    Interval.DiminishedFifth,
    Interval.AugmentedFifth,
    Interval.MinorSeventh,
  ]);
  public static readonly HalfWholeDiminished: ScalePattern = new ScalePattern(
    'Half Whole Diminished',
    [
      Interval.Unison,
      Interval.MinorSecond,
      Interval.MinorThird,
      Interval.MajorThird,
      Interval.AugmentedFourth,
      Interval.PerfectFifth,
      Interval.MajorSixth,
      Interval.MinorSeventh,
    ]
  );
  public static readonly WholeTone: ScalePattern = new ScalePattern('Whole Tone', [
    Interval.Unison,
    Interval.MajorSecond,
    Interval.MajorThird,
    Interval.DiminishedFifth,
    Interval.AugmentedFifth,
    Interval.MinorSeventh,
  ]);
  public static readonly MajorSixthDiminished: ScalePattern = new ScalePattern(
    'Major Sixth Diminished',
    [
      Interval.Unison,
      Interval.MajorSecond,
      Interval.MajorThird,
      Interval.PerfectFourth,
      Interval.PerfectFifth,
      Interval.AugmentedFifth,
      Interval.MajorSixth,
      Interval.MajorSeventh,
    ]
  );
  public static readonly MinorSixthDiminished: ScalePattern = new ScalePattern(
    'Minor Sixth Diminished',
    [
      Interval.Unison,
      Interval.MajorSecond,
      Interval.MinorThird,
      Interval.PerfectFourth,
      Interval.PerfectFifth,
      Interval.AugmentedFifth,
      Interval.MajorSixth,
      Interval.MajorSeventh,
    ]
  );
  public static readonly DominantDiminished: ScalePattern = new ScalePattern(
    'Dominant Diminished',
    [
      Interval.Unison,
      Interval.MajorSecond,
      Interval.MajorThird,
      Interval.PerfectFourth,
      Interval.PerfectFifth,
      Interval.AugmentedFifth,
      Interval.MinorSeventh,
      Interval.MajorSeventh,
    ]
  );
  public static readonly Dominantb5Diminished: ScalePattern = new ScalePattern(
    'Dominant b5 Diminished',
    [
      Interval.Unison,
      Interval.MajorSecond,
      Interval.MajorThird,
      Interval.PerfectFourth,
      Interval.DiminishedFifth,
      Interval.AugmentedFifth,
      Interval.MinorSeventh,
      Interval.MajorSeventh,
    ]
  );

  public createScale(root: Pitch) {
    return new Scale(this, root);
  }

  public createScalePitches(root: Pitch): Pitch[] {
    return this.pattern.map((interval) => root.transpose(interval));
  }

  public get Name(): string {
    return this.name;
  }

  public static get ScalePatterns(): ScalePattern[] {
    return ScalePattern.all;
  }

  public get To(): ScalePatternPrimitives {
    return {
      name: this.Name,
      pattern: this.pattern.map((i) => i.To),
    };
  }

  public equals(other: ScalePattern) {
    return this.name === other.name;
  }

  public static From(primitive: ScalePatternPrimitives) {
    return new ScalePattern(
      primitive.name,
      primitive.pattern.map((i) => Interval.From(i))
    );
  }

  public static readonly MajorScaleModePatterns = [
    ScalePattern.Ionian,
    ScalePattern.Dorian,
    ScalePattern.Phrygian,
    ScalePattern.Lydian,
    ScalePattern.Mixolydian,
    ScalePattern.Aolian,
    ScalePattern.Locrian,
  ];
}

export class Scale implements Iterable<Pitch> {
  constructor(
    private readonly scalePattern: ScalePattern,
    private readonly root: Pitch,
    private readonly pitches: Pitch[] = scalePattern.createScalePitches(root)
  ) {}

  get To(): Readonly<ScalePrimitives> {
    return {
      pattern: this.scalePattern.To,
      root: this.root.To,
      pitches: this.pitches.map((p) => p.To),
    };
  }

  static From(primitive: ScalePrimitives) {
    return new Scale(
      ScalePattern.From(primitive.pattern),
      Pitch.From(primitive.root),
      primitive.pitches.map((p) => Pitch.From(p))
    );
  }

  pitchFor(degree: ScaleDegree) {
    return this.pitches[degree]!;
  }

  degreeFor(pitch: Pitch) {
    return ensure(
      this.pitches.findIndex((p) => p === pitch),
      'Pitch not found in scale'
    );
  }

  down(from: ScaleDegree, to: ScaleDegree) {
    if (from > to) {
      const line = this.pitches.slice(to, from + 1).reverse();
      return new PitchLine(line, PitchLineDirection.Descending);
    }

    const line1 = this.pitches.slice(0, from + 1).reverse();
    const line2 = this.pitches.slice(to).reverse();
    return new PitchLine(line1.concat(line2), PitchLineDirection.Descending);
  }

  melodicLine(duration: Duration, octave: Octave) {
    return new MelodicLine(
      this.pitches.map(
        (p, i) => new Note(p, duration, p === Pitch.C && i !== 0 ? octave.up() : octave)
      )
    );
  }

  thirdsFrom(degree: ScaleDegree): Array<Pitch> {
    return this.pitches
      .slice(degree)
      .concat(this.pitches)
      .concat(this.pitches)
      .filter((_, i) => i % 2 === 0)
      .slice(0, 7);
  }

  melodicThirdsFrom(degree: ScaleDegree, pitchDurations: Duration, octave: Octave) {
    return new MelodicLine(
      this.pitches
        .slice(degree)
        .map((p) => new Note(p, pitchDurations, octave))
        .concat(this.pitches.map((p) => new Note(p, pitchDurations, octave.up())))
        .concat(this.pitches.map((p) => new Note(p, pitchDurations, octave.up().up())))
        .filter((_, i) => i % 2 === 0)
        .slice(0, 7)
    );
  }

  melodicThirdsTo(degree: ScaleDegree, pitchDurations: Duration, startingOctave: Octave) {
    return new MelodicLine(
      this.pitches
        .slice(degree)
        .map((p) => new Note(p, pitchDurations, startingOctave))
        .concat(this.pitches.map((p) => new Note(p, pitchDurations, startingOctave.up())))
        .concat(this.pitches.map((p) => new Note(p, pitchDurations, startingOctave.up().up())))
        .filter((_, i) => i % 2 != 0 || i == 0)
        .slice(0, 7)
    );
  }

  hasPattern(other: ScalePattern) {
    return this.scalePattern.equals(other);
  }

  *[Symbol.iterator](): Iterator<Pitch> {
    for (const pitch of this.pitches) {
      yield pitch;
    }
  }
}

export interface ScaleHarmonizer {
  chordFor(degree: ScaleDegree): Chord;
}

export class TriadHarmonizer implements ScaleHarmonizer {
  constructor(private scale: Scale) {}

  chordFor(degree: ScaleDegree): Chord {
    const thirds = this.scale.thirdsFrom(degree);

    const chordPitches = new ChordPitches([
      new ChordPitch(thirds[0]!, ChordFunction.Root),
      new ChordPitch(thirds[1]!, ChordFunction.Third),
      new ChordPitch(thirds[2]!, ChordFunction.Fifth),
    ]);

    return new ClosedChord(
      chordPitches.pitchForFunction(ChordFunction.Root),
      ChordPattern.patternFor(chordPitches.toIntervals()) ??
        throwExpression('Invalid chord pattern'),
      Duration.Whole,
      Octave.C4
    );
  }
}

export class SeventhHarmonizer implements ScaleHarmonizer {
  constructor(private scale: Scale) {}

  chordFor(degree: ScaleDegree): Chord {
    const thirds = this.scale.thirdsFrom(degree);

    const chordPitches = new ChordPitches([
      new ChordPitch(thirds[0]!, ChordFunction.Root),
      new ChordPitch(thirds[1]!, ChordFunction.Third),
      new ChordPitch(thirds[2]!, ChordFunction.Fifth),
      new ChordPitch(thirds[3]!, ChordFunction.Seventh),
    ]);

    return new ClosedChord(
      chordPitches.pitchForFunction(ChordFunction.Root),
      ChordPattern.patternFor(chordPitches.toIntervals()) ??
        throwExpression('Invalid chord pattern'),
      Duration.Whole,
      Octave.C4
    );
  }
}