pedromsantos/glenn

View on GitHub
src/__test__/Domain/Pitch.test.ts

Summary

Maintainability
F
6 days
Test Coverage
/* eslint-disable jest/no-conditional-expect */
/* eslint-disable sonarjs/cognitive-complexity */

import * as fc from 'fast-check';

import { Duration } from '../../Domain/Duration';
import { Interval } from '../../Domain/Interval';
import { Note, Octave } from '../../Domain/Note';
import { Pitch, PitchLine, PitchLineDirection } from '../../Domain/Pitch';

describe('Pitch line', () => {
  describe('convert to melodic line', () => {
    test('a single pitch', () => {
      const line = new PitchLine([Pitch.C], PitchLineDirection.Ascending);

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
    });

    test('multiple pitches', () => {
      const line = new PitchLine([Pitch.C, Pitch.D, Pitch.E], PitchLineDirection.Ascending);

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
      expect([...melodicLine][1]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C4));
      expect([...melodicLine][2]).toStrictEqual(new Note(Pitch.E, Duration.Eighth, Octave.C4));
    });

    test('multiple pitches when repeated stay on same octave on neutral lines', () => {
      const line = new PitchLine([Pitch.C, Pitch.D, Pitch.E, Pitch.C], PitchLineDirection.Neutral);

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
      expect([...melodicLine][1]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C4));
      expect([...melodicLine][2]).toStrictEqual(new Note(Pitch.E, Duration.Eighth, Octave.C4));
      expect([...melodicLine][3]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
    });

    test('multiple pitches when repeated go up an octave on ascending lines', () => {
      const line = new PitchLine(
        [Pitch.C, Pitch.D, Pitch.E, Pitch.C],
        PitchLineDirection.Ascending
      );

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
      expect([...melodicLine][1]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C4));
      expect([...melodicLine][2]).toStrictEqual(new Note(Pitch.E, Duration.Eighth, Octave.C4));
      expect([...melodicLine][3]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C5));
    });

    test('multiple pitches after 1 repeat go up an octave on ascending lines', () => {
      const line = new PitchLine(
        [Pitch.C, Pitch.D, Pitch.E, Pitch.C, Pitch.D],
        PitchLineDirection.Ascending
      );

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
      expect([...melodicLine][1]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C4));
      expect([...melodicLine][2]).toStrictEqual(new Note(Pitch.E, Duration.Eighth, Octave.C4));
      expect([...melodicLine][3]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C5));
      expect([...melodicLine][4]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C5));
    });

    test('multiple pitches when repeated go down an octave on descending lines', () => {
      const line = new PitchLine(
        [Pitch.C, Pitch.D, Pitch.E, Pitch.C],
        PitchLineDirection.Descending
      );

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
      expect([...melodicLine][1]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C4));
      expect([...melodicLine][2]).toStrictEqual(new Note(Pitch.E, Duration.Eighth, Octave.C4));
      expect([...melodicLine][3]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C3));
    });

    test('multiple pitches after 1 repeat go down an octave on descending lines', () => {
      const line = new PitchLine(
        [Pitch.C, Pitch.D, Pitch.E, Pitch.C, Pitch.B],
        PitchLineDirection.Descending
      );

      const melodicLine = line.melodicLine(Octave.C4, Duration.Eighth);

      expect([...melodicLine][0]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C4));
      expect([...melodicLine][1]).toStrictEqual(new Note(Pitch.D, Duration.Eighth, Octave.C4));
      expect([...melodicLine][2]).toStrictEqual(new Note(Pitch.E, Duration.Eighth, Octave.C4));
      expect([...melodicLine][3]).toStrictEqual(new Note(Pitch.C, Duration.Eighth, Octave.C3));
      expect([...melodicLine][4]).toStrictEqual(new Note(Pitch.B, Duration.Eighth, Octave.C3));
    });
  });
});

describe('Pitch', () => {
  describe('sharp should raise it by half a tone', () => {
    test('sharp C is C#', () => {
      expect(Pitch.C.sharp()).toBe(Pitch.CSharp);
    });

    test('sharp C# is D', () => {
      expect(Pitch.CSharp.sharp()).toBe(Pitch.D);
    });

    test('sharp Db is D', () => {
      expect(Pitch.DFlat.sharp()).toBe(Pitch.D);
    });

    test('sharp D is D#', () => {
      expect(Pitch.D.sharp()).toBe(Pitch.DSharp);
    });

    test('sharp D# is E', () => {
      expect(Pitch.DSharp.sharp()).toBe(Pitch.E);
    });

    test('sharp EFlat is E', () => {
      expect(Pitch.EFlat.sharp()).toBe(Pitch.E);
    });

    test('sharp E is F', () => {
      expect(Pitch.E.sharp()).toBe(Pitch.F);
    });

    test('sharp F is F#', () => {
      expect(Pitch.E.sharp()).toBe(Pitch.F);
    });

    test('sharp F# is G', () => {
      expect(Pitch.FSharp.sharp()).toBe(Pitch.G);
    });

    test('sharp Gb is G', () => {
      expect(Pitch.GFlat.sharp()).toBe(Pitch.G);
    });

    test('sharp G is G#', () => {
      expect(Pitch.G.sharp()).toBe(Pitch.GSharp);
    });

    test('sharp G# is A', () => {
      expect(Pitch.GSharp.sharp()).toBe(Pitch.A);
    });

    test('sharp Ab is G', () => {
      expect(Pitch.AFlat.sharp()).toBe(Pitch.A);
    });

    test('sharp A is A#', () => {
      expect(Pitch.A.sharp()).toBe(Pitch.ASharp);
    });

    test('sharp A# is B', () => {
      expect(Pitch.ASharp.sharp()).toBe(Pitch.B);
    });

    test('sharp Bb is B', () => {
      expect(Pitch.BFlat.sharp()).toBe(Pitch.B);
    });

    test('sharp B is C', () => {
      expect(Pitch.B.sharp()).toBe(Pitch.C);
    });
  });

  describe('flat should lower it by half a tone', () => {
    test('sharp C is B', () => {
      expect(Pitch.C.flat()).toBe(Pitch.B);
    });

    test('flat C# is C', () => {
      expect(Pitch.CSharp.flat()).toBe(Pitch.C);
    });

    test('flat Db is C', () => {
      expect(Pitch.DFlat.flat()).toBe(Pitch.C);
    });

    test('flat D is Db', () => {
      expect(Pitch.D.flat()).toBe(Pitch.DFlat);
    });

    test('flat D# is D', () => {
      expect(Pitch.DSharp.flat()).toBe(Pitch.D);
    });

    test('flat EFlat is D', () => {
      expect(Pitch.EFlat.flat()).toBe(Pitch.D);
    });

    test('flat E is Eb', () => {
      expect(Pitch.E.flat()).toBe(Pitch.EFlat);
    });

    test('flat F is E', () => {
      expect(Pitch.F.flat()).toBe(Pitch.E);
    });

    test('flat F# is G', () => {
      expect(Pitch.FSharp.flat()).toBe(Pitch.F);
    });

    test('flat Gb is F', () => {
      expect(Pitch.GFlat.flat()).toBe(Pitch.F);
    });

    test('flat G is Gb', () => {
      expect(Pitch.G.flat()).toBe(Pitch.GFlat);
    });

    test('sharp G# is G', () => {
      expect(Pitch.GSharp.flat()).toBe(Pitch.G);
    });

    test('flat Ab is G', () => {
      expect(Pitch.AFlat.flat()).toBe(Pitch.G);
    });

    test('flat A is Ab', () => {
      expect(Pitch.A.flat()).toBe(Pitch.AFlat);
    });

    test('flat A# is A', () => {
      expect(Pitch.ASharp.flat()).toBe(Pitch.A);
    });

    test('flat Bb is A', () => {
      expect(Pitch.BFlat.flat()).toBe(Pitch.A);
    });

    test('flat B is Bb', () => {
      expect(Pitch.B.flat()).toBe(Pitch.BFlat);
    });
  });

  describe('measure semitones between', () => {
    test('C and C to zero semitones', () => {
      expect(Pitch.C.absoluteDistance(Pitch.C)).toBe(0);
    });
    test('C and C# to one semitones', () => {
      expect(Pitch.C.absoluteDistance(Pitch.CSharp)).toBe(1);
    });
    test('C and Db to one semitones', () => {
      expect(Pitch.C.absoluteDistance(Pitch.DFlat)).toBe(1);
    });
    test('C and D to one semitones', () => {
      expect(Pitch.C.absoluteDistance(Pitch.D)).toBe(2);
    });

    test('C and E flat to three semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.EFlat)).toBe(3);
    });

    test('C and E to four semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.E)).toBe(4);
    });

    test('C and F to five semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.F)).toBe(5);
    });

    test('C and F sharp to six semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.FSharp)).toBe(6);
    });

    test('C and G flat to six semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.GFlat)).toBe(6);
    });

    test('C and G to seven semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.G)).toBe(7);
    });

    test('C and G sharp to eight semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.GSharp)).toBe(8);
    });

    test('C and A flat to eight semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.AFlat)).toBe(8);
    });

    test('C and A to nine semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.A)).toBe(9);
    });

    test('C and A sharp to ten semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.ASharp)).toBe(10);
    });

    test('C and B flat to ten semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.BFlat)).toBe(10);
    });

    test('C and B to eleven semitones`', () => {
      expect(Pitch.C.absoluteDistance(Pitch.B)).toBe(11);
    });
  });

  describe('measure intervals between', () => {
    test('C and Db to minor second', () => {
      expect(Pitch.C.intervalTo(Pitch.DFlat)).toBe(Interval.MinorSecond);
    });

    test('C and D as MajorSecond', () => {
      expect(Pitch.C.intervalTo(Pitch.D)).toBe(Interval.MajorSecond);
    });

    test('C and E flat as MinorThird', () => {
      expect(Pitch.C.intervalTo(Pitch.EFlat)).toBe(Interval.MinorThird);
    });

    test('C and E as MajorThird', () => {
      expect(Pitch.C.intervalTo(Pitch.E)).toBe(Interval.MajorThird);
    });

    test('C and F as PerfectFourth', () => {
      expect(Pitch.C.intervalTo(Pitch.F)).toBe(Interval.PerfectFourth);
    });

    test('C and F sharp as AugmentedFourth', () => {
      expect(Pitch.C.intervalTo(Pitch.FSharp)).toBe(Interval.AugmentedFourth);
    });

    test('C and G flat as DiminishedFifth', () => {
      expect(Pitch.C.intervalTo(Pitch.GFlat)).toBe(Interval.DiminishedFifth);
    });

    test('C and G as PerfectFifth', () => {
      expect(Pitch.C.intervalTo(Pitch.G)).toBe(Interval.PerfectFifth);
    });

    test('C and G sharp as AugmentedFifth', () => {
      expect(Pitch.C.intervalTo(Pitch.GSharp)).toBe(Interval.AugmentedFifth);
    });

    test('C and A flat as MinorSixth', () => {
      expect(Pitch.C.intervalTo(Pitch.AFlat)).toBe(Interval.MinorSixth);
    });

    test('C and A as MajorSixth', () => {
      expect(Pitch.C.intervalTo(Pitch.A)).toBe(Interval.MajorSixth);
    });

    test('C and B flat as MinorSeventh', () => {
      expect(Pitch.C.intervalTo(Pitch.BFlat)).toBe(Interval.MinorSeventh);
    });

    test('C and B as MajorSeventh', () => {
      expect(Pitch.C.intervalTo(Pitch.B)).toBe(Interval.MajorSeventh);
    });

    test('D and F as MinorThird', () => {
      expect(Pitch.D.intervalTo(Pitch.F)).toBe(Interval.MinorThird);
    });
  });

  describe('transpose using a', () => {
    test('Unison from C to C', () => {
      expect(Pitch.C.transpose(Interval.Unison)).toBe(Pitch.C);
    });

    test('Unison from C# to C#', () => {
      expect(Pitch.CSharp.transpose(Interval.Unison)).toBe(Pitch.CSharp);
    });

    test('MinorSecond from C to D flat', () => {
      expect(Pitch.C.transpose(Interval.MinorSecond)).toBe(Pitch.DFlat);
    });

    test('MajorSecond from C to D', () => {
      expect(Pitch.C.transpose(Interval.MajorSecond)).toBe(Pitch.D);
    });

    test('AugmentedSecond from C to D sharp', () => {
      expect(Pitch.C.transpose(Interval.AugmentedSecond)).toBe(Pitch.DSharp);
    });

    test('MinorThird from C to E flat', () => {
      expect(Pitch.C.transpose(Interval.MinorThird)).toBe(Pitch.EFlat);
    });

    test('MajorThird from C to E', () => {
      expect(Pitch.C.transpose(Interval.MajorThird)).toBe(Pitch.E);
    });

    test('PerfectFourth from C to F', () => {
      expect(Pitch.C.transpose(Interval.PerfectFourth)).toBe(Pitch.F);
    });

    test('AugmentedFourth from C to F sharp', () => {
      expect(Pitch.C.transpose(Interval.AugmentedFourth)).toBe(Pitch.FSharp);
    });

    test('DiminishedFifth from C to G flat', () => {
      expect(Pitch.C.transpose(Interval.DiminishedFifth)).toBe(Pitch.GFlat);
    });

    test('PerfectFifth from C to G', () => {
      expect(Pitch.C.transpose(Interval.PerfectFifth)).toBe(Pitch.G);
    });

    test('PerfectFifth from D to A', () => {
      expect(Pitch.D.transpose(Interval.PerfectFifth)).toBe(Pitch.A);
    });

    test('AugmentedFifth from C to G sharp', () => {
      expect(Pitch.C.transpose(Interval.AugmentedFifth)).toBe(Pitch.GSharp);
    });

    test('MinorSixth from C to A flat', () => {
      expect(Pitch.C.transpose(Interval.MinorSixth)).toBe(Pitch.AFlat);
    });

    test('MajorSixth from C to A', () => {
      expect(Pitch.C.transpose(Interval.MajorSixth)).toBe(Pitch.A);
    });

    test('diminished seventh from C to B flat flat', () => {
      expect(Pitch.C.transpose(Interval.DiminishedSeventh)).toBe(Pitch.A);
    });

    test('diminished seventh from G to E', () => {
      expect(Pitch.G.transpose(Interval.DiminishedSeventh)).toBe(Pitch.E);
    });

    test('minor seventh from C to B flat', () => {
      expect(Pitch.C.transpose(Interval.MinorSeventh)).toBe(Pitch.BFlat);
    });

    test('MajorSeventh from C to B', () => {
      expect(Pitch.C.transpose(Interval.MajorSeventh)).toBe(Pitch.B);
    });

    test('augmented ninth from G to A#', () => {
      expect(Pitch.G.transpose(Interval.AugmentedNinth)).toBe(Pitch.ASharp);
    });

    test('perfect eleventh from G to C', () => {
      expect(Pitch.G.transpose(Interval.PerfectEleventh)).toBe(Pitch.C);
    });

    test('augmented second from D to E#', () => {
      expect(Pitch.D.transpose(Interval.AugmentedSecond)).toBe(Pitch.ESharp);
    });

    test('minor third from G to Bb', () => {
      expect(Pitch.G.transpose(Interval.MinorThird)).toBe(Pitch.BFlat);
    });

    test('major third from G to B', () => {
      expect(Pitch.G.transpose(Interval.MajorThird)).toBe(Pitch.B);
    });
  });

  describe('properties', () => {
    test('sharping and flattening a pitch results in the original note pitch', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), (pitch) => {
          const newPitch = pitch.sharp().flat();

          expect(newPitch.NumericValue).toBe(pitch.NumericValue);
        })
      );
    });

    test('The natural pitch of a pitch starts with the same pitch name', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), (pitch) => {
          const newPitch = pitch.natural();

          expect(newPitch.Name[0]).toBe(pitch.Name[0]);
        })
      );
    });

    test('flating and sharping a pitch results in the original note pitch', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), (pitch) => {
          const newPitch = pitch.flat().sharp();

          expect(newPitch.NumericValue).toBe(pitch.NumericValue);
        })
      );
    });

    test('a sharped note has a higher pitch except B', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), (pitch) => {
          if (pitch === Pitch.B) {
            expect(pitch.sharp().NumericValue).toBeLessThan(pitch.NumericValue);
          } else {
            expect(pitch.sharp().NumericValue).toBeGreaterThan(pitch.NumericValue);
          }
        })
      );
    });

    test('a flatted note has a lower pitch except C', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), (pitch) => {
          if (pitch === Pitch.C) {
            expect(pitch.flat().NumericValue).toBeGreaterThan(pitch.NumericValue);
          } else {
            expect(pitch.flat().NumericValue).toBeLessThan(pitch.NumericValue);
          }
        })
      );
    });

    test('pitch can be converted to and from PitchPrimitives', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), (pitch) => {
          const from = Pitch.From(pitch.To);

          expect(pitch.NumericValue).toBe(from?.NumericValue);
        })
      );
    });

    test('measure semitones between a note and itself sharp n times to n semitones', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), fc.nat({ max: 12 }), (pitch, distance) => {
          let transposed = pitch;

          for (let i = 0; i < distance; i++) {
            transposed = transposed.sharp();
          }

          if (distance === 12) {
            expect(pitch.absoluteDistance(transposed)).toBe(0);
          } else {
            expect(pitch.absoluteDistance(transposed)).toBe(distance);
          }
        })
      );
    });

    test('measure semitones between a note and itself flat n times to n semitones', () => {
      fc.assert(
        fc.property(fc.constantFrom(...Pitch.pitches), fc.nat({ max: 12 }), (pitch, distance) => {
          let transposed = pitch;

          for (let i = 0; i < distance; i++) {
            transposed = transposed.flat();
          }

          if (distance === 12 || distance === 0) {
            expect(pitch.absoluteDistance(transposed)).toBe(0);
          } else {
            expect(pitch.absoluteDistance(transposed)).toBe(12 - distance);
          }
        })
      );
    });

    test('measure interval between a pitch and itself transposed by an interval to be the interval', () => {
      fc.assert(
        fc.property(
          fc.constantFrom(...Pitch.pitches),
          fc.constantFrom(...Interval.intervals),
          (pitch, interval) => {
            const to = pitch.transpose(interval);
            const resultingInterval = pitch.intervalTo(to);

            switch (interval) {
              case Interval.MajorNinth:
                expect(resultingInterval).toBe(Interval.MajorSecond);
                break;
              case Interval.PerfectEleventh:
                expect(resultingInterval).toBe(Interval.PerfectFourth);
                break;
              case Interval.AugmentedEleventh:
                expect(resultingInterval).toBe(Interval.AugmentedFourth);
                break;
              case Interval.MajorThirteenth:
                expect(resultingInterval).toBe(Interval.MajorSixth);
                break;
              case Interval.PerfectOctave:
                expect(resultingInterval).toBe(Interval.Unison);
                break;
              case Interval.DiminishedSeventh:
                if (
                  resultingInterval === Interval.MajorSixth ||
                  resultingInterval === Interval.DiminishedSeventh
                )
                  expect(resultingInterval).toBe(resultingInterval);
                break;
              case Interval.MinorThird:
              case Interval.AugmentedNinth:
              case Interval.AugmentedSecond:
                if (
                  resultingInterval === Interval.MinorThird ||
                  resultingInterval === Interval.AugmentedSecond
                )
                  expect(resultingInterval).toBe(resultingInterval);
                break;
              case Interval.MinorThirteenth:
              case Interval.MinorSixth:
              case Interval.AugmentedFifth:
                if (
                  resultingInterval === Interval.MinorThirteenth ||
                  resultingInterval === Interval.MinorSixth ||
                  resultingInterval === Interval.AugmentedFifth
                )
                  expect(resultingInterval).toBe(resultingInterval);
                break;
              case Interval.Tritone:
              case Interval.DiminishedFifth:
              case Interval.AugmentedFourth:
                if (
                  resultingInterval === Interval.AugmentedFourth ||
                  resultingInterval === Interval.DiminishedFifth
                )
                  expect(resultingInterval).toBe(resultingInterval);
                break;
              case Interval.MinorNinth:
              case Interval.MinorSecond:
              case Interval.AugmentedUnison:
                if (
                  resultingInterval === Interval.MinorNinth ||
                  resultingInterval === Interval.MinorSecond ||
                  resultingInterval === Interval.AugmentedUnison
                )
                  expect(resultingInterval).toBe(resultingInterval);
                break;
              case Interval.DiminishedUnison:
              case Interval.DiminishedNinth:
                expect(resultingInterval).toBe(resultingInterval);
                break;
              default:
                expect(resultingInterval).toBe(interval);
            }
          }
        )
      );
    });
  });
});