120 lines
4.0 KiB
TypeScript
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);
|
|
});
|
|
});
|