Files
KGStudio/src/util/chordGuideDataUtil.test.ts
T

120 lines
4.0 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import { Note } from 'tonal';
import { KGCore } from '../core/KGCore';
import type { ChordGuideData } from '../core/ChordGuideTypes';
import { getMatchingChordGuideChordsForPitch, resolveChordGuideItems } from './chordGuideDataUtil';
import { getChordPitchClasses, parseChordSymbol } from './chordUtil';
import chordGuideDataJson from '../../public/resources/modes/chord_guide.json';
const chordGuideData = chordGuideDataJson as ChordGuideData;
function getExpectedPitchClassesFromNotes(notes: string[]): number[] {
return notes.map((note) => {
const parsed = Note.get(note);
if (parsed.empty || parsed.chroma === undefined) {
throw new Error(`Unable to convert note "${note}" to pitch class`);
}
return parsed.chroma;
});
}
describe('chordGuideDataUtil', () => {
beforeEach(() => {
KGCore.CHORD_GUIDE_DATA = chordGuideData;
KGCore.FUNCTIONAL_CHORDS_DATA = {
ionian: { name: 'Broken', steps: [2, 2, 2, 2, 2, 1, 1], T: [], S: [], D: [], chords: {} },
};
});
it('parses every chord symbol used by chord_guide.json', () => {
const allItems = [
...chordGuideData.ionian.T,
...chordGuideData.ionian.S,
...chordGuideData.ionian.D,
...chordGuideData.aeolian.T,
...chordGuideData.aeolian.S,
...chordGuideData.aeolian.D,
];
for (const item of allItems) {
const parsed = parseChordSymbol(item.name);
expect(parsed, item.name).not.toBeNull();
expect(parsed?.symbol).toBe(item.name);
}
});
it('maps every chord guide symbol to the corresponding notes declared in chord_guide.json', () => {
const allItems = [
...chordGuideData.ionian.T,
...chordGuideData.ionian.S,
...chordGuideData.ionian.D,
...chordGuideData.aeolian.T,
...chordGuideData.aeolian.S,
...chordGuideData.aeolian.D,
];
for (const item of allItems) {
const actualPitchClasses = getChordPitchClasses(item.name).map((pitch) => ((pitch % 12) + 12) % 12);
const expectedPitchClasses = getExpectedPitchClassesFromNotes(item.notes);
expect(
actualPitchClasses,
`${item.name} should resolve to ${item.notes.join(' ')}`
).toEqual(expectedPitchClasses);
}
});
it('resolves ionian tonic chords in C major from the new chord guide data', () => {
const result = resolveChordGuideItems('C major', 'ionian', 'T');
expect(result[0]).toMatchObject({
name: 'C',
roman: 'I',
notes: ['C', 'E', 'G'],
resolvedNotes: ['C', 'E', 'G'],
pitchClasses: [0, 4, 7],
});
expect(result.find((item) => item.name === 'Am')?.pitchClasses).toEqual([9, 12, 16]);
});
it('transposes ionian tonic chords away from C major', () => {
const result = resolveChordGuideItems('D major', 'ionian', 'T');
expect(result[0]).toMatchObject({
name: 'C',
resolvedNotes: ['D', 'F#', 'A'],
pitchClasses: [2, 6, 9],
});
expect(result.find((item) => item.name === 'Am')?.resolvedNotes).toEqual(['B', 'D', 'F#']);
});
it('uses A minor as the aeolian reference tonic', () => {
const result = resolveChordGuideItems('A minor', 'aeolian', 'T');
expect(result[0]).toMatchObject({
name: 'Am',
resolvedNotes: ['A', 'C', 'E'],
pitchClasses: [9, 12, 16],
});
expect(result.find((item) => item.name === 'Amadd9')?.resolvedNotes).toEqual(['A', 'C', 'E', 'B']);
});
it('transposes aeolian tonic chords away from A minor', () => {
const result = resolveChordGuideItems('E minor', 'aeolian', 'T');
expect(result[0]).toMatchObject({
name: 'Am',
resolvedNotes: ['E', 'G', 'B'],
pitchClasses: [4, 7, 11],
});
expect(result.find((item) => item.name === 'C')?.resolvedNotes).toEqual(['G', 'B', 'D']);
});
it('matches hover chords without depending on functional chord config', () => {
const result = getMatchingChordGuideChordsForPitch(60, 'C major', 'ionian', 'T');
expect(result[0]).toEqual([0, 4, 7]);
expect(result.some((chord) => chord.includes(0))).toBe(true);
});
});