Files
KGStudio/src/util/chordGuideDataUtil.ts
T

135 lines
4.0 KiB
TypeScript

import { KGCore } from '../core/KGCore';
import type { KeySignature } from '../core/KGProject';
import type { ChordGuideData, ChordGuideItem, ResolvedChordGuideItem } from '../core/ChordGuideTypes';
import { getChordMidiPitches, parseChordSymbol } from './chordUtil';
import { getRootNoteFromKeySignature, noteNameToPitchClass } from './scaleUtil';
const SHARP_NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
export type ChordGuideMode = 'ionian' | 'aeolian';
export type ChordGuideFunctionType = 'T' | 'S' | 'D';
export interface MatchingChordGuideCandidate {
item: ResolvedChordGuideItem;
matchedPitchIndex: number;
displayPitchClasses: number[];
}
function getReferenceTonic(mode: ChordGuideMode): string {
return mode === 'aeolian' ? 'A' : 'C';
}
function getChordGuideData(): ChordGuideData {
return KGCore.CHORD_GUIDE_DATA ?? {
ionian: { T: [], S: [], D: [] },
aeolian: { T: [], S: [], D: [] },
};
}
function transposePitchClass(pitchClass: number, semitones: number): number {
return (pitchClass + semitones + 120) % 12;
}
function normalizeSemitoneOffset(semitones: number): number {
return ((semitones % 12) + 12) % 12;
}
function toResolvedNoteNames(pitches: number[]): string[] {
return pitches.map((pitch) => SHARP_NOTE_NAMES[((pitch % 12) + 12) % 12]);
}
function resolveChordRootPitchClass(item: ChordGuideItem, semitones: number): number | null {
const descriptor = parseChordSymbol(item.name);
if (!descriptor) {
console.warn(`Unable to parse chord guide symbol: ${item.name}`);
return null;
}
return transposePitchClass(noteNameToPitchClass(descriptor.root), semitones);
}
export function resolveChordGuideItems(
keySignature: KeySignature,
mode: ChordGuideMode,
functionType: ChordGuideFunctionType
): ResolvedChordGuideItem[] {
const data = getChordGuideData()[mode];
if (!data) {
return [];
}
const items = data[functionType] ?? [];
const referenceTonic = getReferenceTonic(mode);
const targetTonic = getRootNoteFromKeySignature(keySignature);
const semitoneOffset = normalizeSemitoneOffset(
noteNameToPitchClass(targetTonic) - noteNameToPitchClass(referenceTonic)
);
return items.flatMap((item) => {
const rootPitchClass = resolveChordRootPitchClass(item, semitoneOffset);
if (rootPitchClass === null) {
return [];
}
const pitchClasses = getChordMidiPitches(item.name, rootPitchClass);
if (pitchClasses.length === 0) {
console.warn(`Unable to resolve pitch classes for chord guide symbol: ${item.name}`);
return [];
}
return [{
...item,
resolvedNotes: toResolvedNoteNames(pitchClasses),
pitchClasses,
}];
});
}
export function getMatchingChordGuideChordsForPitch(
hoverPitch: number,
keySignature: KeySignature,
mode: ChordGuideMode,
functionType: ChordGuideFunctionType
): number[][] {
return getMatchingChordGuideCandidatesForPitch(hoverPitch, keySignature, mode, functionType)
.map((candidate) => candidate.displayPitchClasses);
}
export function getMatchingChordGuideCandidatesForPitch(
hoverPitch: number,
keySignature: KeySignature,
mode: ChordGuideMode,
functionType: ChordGuideFunctionType
): MatchingChordGuideCandidate[] {
const resolvedChords = resolveChordGuideItems(keySignature, mode, functionType);
if (resolvedChords.length === 0) {
return [];
}
const hoverPitchClass = ((hoverPitch % 12) + 12) % 12;
const matchesByPosition: MatchingChordGuideCandidate[][] = [];
for (const item of resolvedChords) {
for (let i = 0; i < item.pitchClasses.length; i++) {
if (item.pitchClasses[i] % 12 !== hoverPitchClass) {
continue;
}
if (!matchesByPosition[i]) {
matchesByPosition[i] = [];
}
matchesByPosition[i].push({
item,
matchedPitchIndex: i,
displayPitchClasses: item.pitchClasses[i] >= 12
? item.pitchClasses.map((pitch) => pitch - 12)
: item.pitchClasses,
});
break;
}
}
return matchesByPosition.flatMap((matches) => matches ?? []);
}