feat: added logic to get suitable chords (console log only)
This commit is contained in:
+35
-14
@@ -12,21 +12,30 @@ import { SettingsPanel } from './components/settings';
|
|||||||
import LoadingOverlay from './components/common/LoadingOverlay';
|
import LoadingOverlay from './components/common/LoadingOverlay';
|
||||||
import { useEffect as useEffectReact, useState, useRef } from 'react';
|
import { useEffect as useEffectReact, useState, useRef } from 'react';
|
||||||
import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool';
|
import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool';
|
||||||
import { KGPianoRollState } from './core/state/KGPianoRollState';
|
import { KGCore } from './core/KGCore';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Enable global keyboard handler for copy/paste and undo/redo
|
// Enable global keyboard handler for copy/paste and undo/redo
|
||||||
useGlobalKeyboardHandler();
|
useGlobalKeyboardHandler();
|
||||||
|
|
||||||
// Use project store instead of local state for project name and tracks
|
// Use project store instead of local state for project name and tracks
|
||||||
const {
|
const {
|
||||||
refreshStatus,
|
refreshStatus,
|
||||||
loadProject, maxBars, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
||||||
showInstrumentSelection
|
showInstrumentSelection
|
||||||
} = useProjectStore();
|
} = useProjectStore();
|
||||||
|
|
||||||
|
// Track if app has been initialized to prevent multiple initializations
|
||||||
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
// Load project when component mounts
|
// Load project when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Guard against multiple initializations (can happen due to React Strict Mode or rerenders)
|
||||||
|
if (hasInitialized.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasInitialized.current = true;
|
||||||
|
|
||||||
const initializeApp = async () => {
|
const initializeApp = async () => {
|
||||||
// Load the current project from KGCore
|
// Load the current project from KGCore
|
||||||
loadProject(null);
|
loadProject(null);
|
||||||
@@ -34,27 +43,39 @@ function App() {
|
|||||||
// Initialize store from config after ConfigManager is ready
|
// Initialize store from config after ConfigManager is ready
|
||||||
await initializeFromConfig();
|
await initializeFromConfig();
|
||||||
|
|
||||||
// Load mode list from JSON
|
// Load all mode and chord data files in parallel
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.BASE_URL}resources/modes/mode_list.json`);
|
const [modeListResponse, functionalChordsResponse] = await Promise.all([
|
||||||
const data = await response.json();
|
fetch(`${import.meta.env.BASE_URL}resources/modes/mode_list.json`),
|
||||||
|
fetch(`${import.meta.env.BASE_URL}resources/modes/functional_chords.json`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [modeListData, functionalChordsData] = await Promise.all([
|
||||||
|
modeListResponse.json(),
|
||||||
|
functionalChordsResponse.json()
|
||||||
|
]);
|
||||||
|
|
||||||
// Store mode data with id, name, and steps
|
// Store mode data with id, name, and steps
|
||||||
KGPianoRollState.MODE_DATA = data.modes;
|
KGCore.MODE_DATA = modeListData.modes;
|
||||||
|
console.log(`Loaded ${modeListData.modes.length} modes:`, modeListData.modes.map((m: { name: string }) => m.name));
|
||||||
|
|
||||||
console.log(`Loaded ${data.modes.length} modes:`, data.modes.map((m: { name: string }) => m.name));
|
// Store functional chords data (T/S/D by mode, including mode-specific chord notes)
|
||||||
|
KGCore.FUNCTIONAL_CHORDS_DATA = functionalChordsData;
|
||||||
|
console.log(`Loaded functional chords for ${Object.keys(functionalChordsData).length} modes`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load mode list:', error);
|
console.error('Failed to load mode/chord data:', error);
|
||||||
// Fallback to default mode
|
// Fallback to defaults
|
||||||
KGPianoRollState.MODE_DATA = [{ id: 'ionian', name: 'Ionian', steps: [2, 2, 1, 2, 2, 2, 1] }];
|
KGCore.MODE_DATA = [{ id: 'ionian', name: 'Ionian', steps: [2, 2, 1, 2, 2, 2, 1] }];
|
||||||
|
KGCore.FUNCTIONAL_CHORDS_DATA = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log maxBars to console
|
// Log maxBars after initialization completes
|
||||||
console.log(`Project max bars: ${maxBars}`);
|
const currentMaxBars = useProjectStore.getState().maxBars;
|
||||||
|
console.log(`Project max bars: ${currentMaxBars}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeApp();
|
initializeApp();
|
||||||
}, [loadProject, maxBars, initializeFromConfig]);
|
}, [loadProject, initializeFromConfig]);
|
||||||
|
|
||||||
// Refresh status periodically to ensure UI is in sync with KGCore
|
// Refresh status periodically to ensure UI is in sync with KGCore
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
|||||||
import { ConfigManager } from '../../core/config/ConfigManager';
|
import { ConfigManager } from '../../core/config/ConfigManager';
|
||||||
import { beatsToBar } from '../../util/midiUtil';
|
import { beatsToBar } from '../../util/midiUtil';
|
||||||
import { UpdateRegionCommand } from '../../core/commands';
|
import { UpdateRegionCommand } from '../../core/commands';
|
||||||
|
import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil';
|
||||||
|
|
||||||
interface PianoRollProps {
|
interface PianoRollProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -39,6 +40,9 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
// Snapping state
|
// Snapping state
|
||||||
const [snapping, setSnapping] = useState<string>('NO SNAP');
|
const [snapping, setSnapping] = useState<string>('NO SNAP');
|
||||||
|
|
||||||
|
// Chord guide state
|
||||||
|
const [chordGuide, setChordGuide] = useState<string>('N');
|
||||||
|
|
||||||
// Piano roll state with temporary initial values
|
// Piano roll state with temporary initial values
|
||||||
const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 });
|
const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 });
|
||||||
|
|
||||||
@@ -303,7 +307,62 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
console.log(`Selected mode: ${value}`);
|
console.log(`Selected mode: ${value}`);
|
||||||
}
|
}
|
||||||
}, [setSelectedMode]);
|
}, [setSelectedMode]);
|
||||||
|
|
||||||
|
// Handle chord guide selection
|
||||||
|
const handleChordGuideSelect = useCallback((value: string) => {
|
||||||
|
setChordGuide(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update suitable chords whenever chord guide, key signature, or mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
const pianoRollState = KGPianoRollState.instance();
|
||||||
|
|
||||||
|
if (chordGuide === 'N') {
|
||||||
|
// Disabled - clear chord data
|
||||||
|
pianoRollState.setCurrentSuitableChords({});
|
||||||
|
pianoRollState.setCurrentSuitableChordsPitchClasses({});
|
||||||
|
|
||||||
|
if (DEBUG_MODE.PIANO_ROLL) {
|
||||||
|
console.log(`Chord guide disabled - cleared suitable chords`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get suitable chords for the selected function (T/S/D)
|
||||||
|
const functionType = chordGuide as 'T' | 'S' | 'D';
|
||||||
|
const suitableChords = getSuitableChords(keySignature, selectedMode, functionType);
|
||||||
|
|
||||||
|
// Convert note names to pitch classes (ensuring ascending order)
|
||||||
|
const chordsPitchClasses: Record<string, number[]> = {};
|
||||||
|
for (const [chordSymbol, noteNames] of Object.entries(suitableChords)) {
|
||||||
|
const pitchClasses: number[] = [];
|
||||||
|
let previousPitch = -1;
|
||||||
|
|
||||||
|
for (const noteName of noteNames) {
|
||||||
|
let pitchClass = noteNameToPitchClass(noteName);
|
||||||
|
|
||||||
|
// If this pitch is lower than the previous one, add an octave
|
||||||
|
if (previousPitch >= 0 && pitchClass <= previousPitch) {
|
||||||
|
pitchClass += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
pitchClasses.push(pitchClass);
|
||||||
|
previousPitch = pitchClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
chordsPitchClasses[chordSymbol] = pitchClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update piano roll state
|
||||||
|
pianoRollState.setCurrentSuitableChords(suitableChords);
|
||||||
|
pianoRollState.setCurrentSuitableChordsPitchClasses(chordsPitchClasses);
|
||||||
|
|
||||||
|
if (DEBUG_MODE.PIANO_ROLL) {
|
||||||
|
console.log(`Chord guide updated: ${chordGuide} (${functionType})`);
|
||||||
|
console.log(`Suitable chords for ${keySignature} in ${selectedMode} mode:`, suitableChords);
|
||||||
|
console.log(`Pitch classes:`, chordsPitchClasses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [chordGuide, keySignature, selectedMode]);
|
||||||
|
|
||||||
// Handler for receiving the setNoteUpdateCounter function from PianoRollContent
|
// Handler for receiving the setNoteUpdateCounter function from PianoRollContent
|
||||||
const handleSetNoteUpdateTrigger = (setNoteFn: React.Dispatch<React.SetStateAction<number>>) => {
|
const handleSetNoteUpdateTrigger = (setNoteFn: React.Dispatch<React.SetStateAction<number>>) => {
|
||||||
triggerNoteUpdateRef.current = setNoteFn;
|
triggerNoteUpdateRef.current = setNoteFn;
|
||||||
@@ -775,6 +834,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
onSnappingSelect={handleSnappingSelect}
|
onSnappingSelect={handleSnappingSelect}
|
||||||
selectedMode={selectedMode}
|
selectedMode={selectedMode}
|
||||||
onModeChange={handleModeSelect}
|
onModeChange={handleModeSelect}
|
||||||
|
chordGuide={chordGuide}
|
||||||
|
onChordGuideChange={handleChordGuideSelect}
|
||||||
blinkButton={blinkButton}
|
blinkButton={blinkButton}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { FaMousePointer, FaPencilAlt } from 'react-icons/fa';
|
import { FaMousePointer, FaPencilAlt } from 'react-icons/fa';
|
||||||
import { KGDropdown } from '../common';
|
import { KGDropdown } from '../common';
|
||||||
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
||||||
|
import { KGCore } from '../../core/KGCore';
|
||||||
|
|
||||||
interface PianoRollToolbarProps {
|
interface PianoRollToolbarProps {
|
||||||
activeTool: 'pointer' | 'pencil';
|
activeTool: 'pointer' | 'pencil';
|
||||||
@@ -13,6 +14,8 @@ interface PianoRollToolbarProps {
|
|||||||
onSnappingSelect: (value: string) => void;
|
onSnappingSelect: (value: string) => void;
|
||||||
selectedMode: string;
|
selectedMode: string;
|
||||||
onModeChange: (value: string) => void;
|
onModeChange: (value: string) => void;
|
||||||
|
chordGuide: string;
|
||||||
|
onChordGuideChange: (value: string) => void;
|
||||||
blinkButton?: string | null;
|
blinkButton?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,20 +29,35 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
|||||||
onSnappingSelect,
|
onSnappingSelect,
|
||||||
selectedMode,
|
selectedMode,
|
||||||
onModeChange,
|
onModeChange,
|
||||||
|
chordGuide,
|
||||||
|
onChordGuideChange,
|
||||||
blinkButton = null
|
blinkButton = null
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="piano-roll-toolbar">
|
<div className="piano-roll-toolbar">
|
||||||
<div className="toolbar-left">
|
<div className="toolbar-left">
|
||||||
{/* Left section with mode dropdown */}
|
{/* Left section with mode and chord guide dropdowns */}
|
||||||
<KGDropdown
|
<KGDropdown
|
||||||
options={KGPianoRollState.MODE_DATA.map(m => ({ label: m.name, value: m.id }))}
|
options={KGCore.MODE_DATA.map(m => ({ label: m.name, value: m.id }))}
|
||||||
value={selectedMode}
|
value={selectedMode}
|
||||||
onChange={(value) => onModeChange(value)}
|
onChange={(value) => onModeChange(value)}
|
||||||
label="Mode"
|
label="Mode"
|
||||||
buttonClassName="mode-dropdown"
|
buttonClassName="mode-dropdown"
|
||||||
showValueAsLabel={true}
|
showValueAsLabel={true}
|
||||||
/>
|
/>
|
||||||
|
<KGDropdown
|
||||||
|
options={[
|
||||||
|
{ label: 'Guide: Disabled', value: 'N' },
|
||||||
|
{ label: 'Chord Guide: T', value: 'T' },
|
||||||
|
{ label: 'Chord Guide: S', value: 'S' },
|
||||||
|
{ label: 'Chord Guide: D', value: 'D' }
|
||||||
|
]}
|
||||||
|
value={chordGuide}
|
||||||
|
onChange={(value) => onChordGuideChange(value)}
|
||||||
|
label="Chord"
|
||||||
|
buttonClassName="chord-guide-dropdown"
|
||||||
|
showValueAsLabel={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="toolbar-center">
|
<div className="toolbar-center">
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export class KGCore {
|
|||||||
// Private static instance for singleton pattern
|
// Private static instance for singleton pattern
|
||||||
private static _instance: KGCore | null = null;
|
private static _instance: KGCore | null = null;
|
||||||
|
|
||||||
|
// Global music data resources
|
||||||
|
public static MODE_DATA: Array<{ id: string; name: string; steps: number[] }> = []; // Modes with id, display name, and interval steps
|
||||||
|
public static FUNCTIONAL_CHORDS_DATA: Record<string, { T: string[]; S: string[]; D: string[]; chords: Record<string, string[]> }> = {}; // Functional chords by mode (T/S/D) with mode-specific chord notes
|
||||||
|
|
||||||
private currentProject: KGProject = new KGProject();
|
private currentProject: KGProject = new KGProject();
|
||||||
|
|
||||||
private status: string = "Ready";
|
private status: string = "Ready";
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ export class KGPianoRollState {
|
|||||||
public static SNAP_OPTIONS: string[] = ['NO SNAP', '1/3', '1/4', '1/6', '1/8', '1/12', '1/16', '1/24', '1/32'];
|
public static SNAP_OPTIONS: string[] = ['NO SNAP', '1/3', '1/4', '1/6', '1/8', '1/12', '1/16', '1/24', '1/32'];
|
||||||
public static QUANT_POS_OPTIONS: string[] = ['1/3', '1/4', '1/6', '1/8', '1/12', '1/16', '1/24', '1/32'];
|
public static QUANT_POS_OPTIONS: string[] = ['1/3', '1/4', '1/6', '1/8', '1/12', '1/16', '1/24', '1/32'];
|
||||||
public static QUANT_LEN_OPTIONS: string[] = ['1/1', '1/2', '1/3', '1/4', '1/6', '1/8', '1/12', '1/16', '1/24', '1/32'];
|
public static QUANT_LEN_OPTIONS: string[] = ['1/1', '1/2', '1/3', '1/4', '1/6', '1/8', '1/12', '1/16', '1/24', '1/32'];
|
||||||
public static MODE_DATA: Array<{ id: string; name: string; steps: number[] }> = []; // Modes with id, display name, and interval steps
|
|
||||||
|
|
||||||
private activeTool: string = "pointer";
|
private activeTool: string = "pointer";
|
||||||
private currentSnap: string = "NO SNAP";
|
private currentSnap: string = "NO SNAP";
|
||||||
private lastEditedNoteLength: number = 1; // Default to 1 beat
|
private lastEditedNoteLength: number = 1; // Default to 1 beat
|
||||||
private currentMode: string = "ionian"; // Default mode
|
private currentMode: string = "ionian"; // Default mode
|
||||||
|
|
||||||
|
// Chord guide state
|
||||||
|
private currentSuitableChords: Record<string, string[]> = {}; // Map of chord symbols to note names (e.g., {"I": ["C", "E", "G"]})
|
||||||
|
private currentSuitableChordsPitchClasses: Record<string, number[]> = {}; // Map of chord symbols to pitch classes (e.g., {"I": [0, 4, 7]})
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
console.log("KGPianoRollState initialized");
|
console.log("KGPianoRollState initialized");
|
||||||
}
|
}
|
||||||
@@ -58,4 +61,20 @@ export class KGPianoRollState {
|
|||||||
public setCurrentMode(mode: string): void {
|
public setCurrentMode(mode: string): void {
|
||||||
this.currentMode = mode;
|
this.currentMode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCurrentSuitableChords(): Record<string, string[]> {
|
||||||
|
return this.currentSuitableChords;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCurrentSuitableChords(chords: Record<string, string[]>): void {
|
||||||
|
this.currentSuitableChords = chords;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrentSuitableChordsPitchClasses(): Record<string, number[]> {
|
||||||
|
return this.currentSuitableChordsPitchClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCurrentSuitableChordsPitchClasses(chordsPitchClasses: Record<string, number[]>): void {
|
||||||
|
this.currentSuitableChordsPitchClasses = chordsPitchClasses;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+133
-2
@@ -1,4 +1,4 @@
|
|||||||
import { KGPianoRollState } from '../core/state/KGPianoRollState';
|
import { KGCore } from '../core/KGCore';
|
||||||
import { pianoRollIndexToPitch } from './midiUtil';
|
import { pianoRollIndexToPitch } from './midiUtil';
|
||||||
import type { KeySignature } from '../core/KGProject';
|
import type { KeySignature } from '../core/KGProject';
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export const getScalePitchClasses = (rootNote: string, modeSteps: number[]): num
|
|||||||
* @returns Array of interval steps, or default ionian if not found
|
* @returns Array of interval steps, or default ionian if not found
|
||||||
*/
|
*/
|
||||||
export const getModeSteps = (modeId: string): number[] => {
|
export const getModeSteps = (modeId: string): number[] => {
|
||||||
const modeData = KGPianoRollState.MODE_DATA.find(m => m.id === modeId);
|
const modeData = KGCore.MODE_DATA.find(m => m.id === modeId);
|
||||||
if (!modeData) {
|
if (!modeData) {
|
||||||
console.warn(`Mode not found: ${modeId}, defaulting to ionian`);
|
console.warn(`Mode not found: ${modeId}, defaulting to ionian`);
|
||||||
return [2, 2, 1, 2, 2, 2, 1]; // Default to ionian (major scale)
|
return [2, 2, 1, 2, 2, 2, 1]; // Default to ionian (major scale)
|
||||||
@@ -74,6 +74,137 @@ export const getModeSteps = (modeId: string): number[] => {
|
|||||||
return modeData.steps;
|
return modeData.steps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transposes a note name by a given number of semitones
|
||||||
|
* @param noteName - Note name like "C", "C#", "Db"
|
||||||
|
* @param semitones - Number of semitones to transpose (positive or negative)
|
||||||
|
* @returns Transposed note name
|
||||||
|
*/
|
||||||
|
const transposeNote = (noteName: string, semitones: number): string => {
|
||||||
|
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||||
|
const pitchClass = noteNameToPitchClass(noteName);
|
||||||
|
const newPitchClass = (pitchClass + semitones + 12) % 12;
|
||||||
|
return noteNames[newPitchClass];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of suitable chords for a given key, mode, and functional harmony group
|
||||||
|
* @param keySignature - Key signature like "C major", "F# minor", "Bb major"
|
||||||
|
* @param modeId - Mode ID (e.g., "ionian", "aeolian", "dorian")
|
||||||
|
* @param functionType - Functional harmony group: "T" (Tonic), "S" (Subdominant), or "D" (Dominant)
|
||||||
|
* @returns Map of chord symbols to their transposed note arrays, e.g., { "I": ["C", "E", "G"], "vi": ["A", "C", "E"] }
|
||||||
|
* Returns empty object if mode or function not found
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* - getSuitableChords("C major", "ionian", "T") returns { "I": ["C", "E", "G"], "vi": ["A", "C", "E"], ... }
|
||||||
|
* - getSuitableChords("D major", "ionian", "T") returns { "I": ["D", "F#", "A"], "vi": ["B", "D", "F#"], ... }
|
||||||
|
*/
|
||||||
|
export const getSuitableChords = (
|
||||||
|
keySignature: KeySignature,
|
||||||
|
modeId: string,
|
||||||
|
functionType: 'T' | 'S' | 'D'
|
||||||
|
): Record<string, string[]> => {
|
||||||
|
// Get the functional chords for this mode
|
||||||
|
const functionalChords = KGCore.FUNCTIONAL_CHORDS_DATA[modeId];
|
||||||
|
if (!functionalChords) {
|
||||||
|
console.warn(`No functional chords found for mode: ${modeId}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the chord symbols for the specified function (T/S/D)
|
||||||
|
const chordSymbols = functionalChords[functionType];
|
||||||
|
if (!chordSymbols || chordSymbols.length === 0) {
|
||||||
|
console.warn(`No chords found for function ${functionType} in mode ${modeId}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mode-specific chords data
|
||||||
|
const modeChords = functionalChords.chords;
|
||||||
|
if (!modeChords) {
|
||||||
|
console.warn(`No chords data found for mode: ${modeId}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the root note from key signature
|
||||||
|
const rootNote = getRootNoteFromKeySignature(keySignature);
|
||||||
|
|
||||||
|
// Calculate the transposition interval from C to the root note
|
||||||
|
const transposeSemitones = noteNameToPitchClass(rootNote);
|
||||||
|
|
||||||
|
// Build a map of chord symbols to transposed notes
|
||||||
|
const chordMap: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const chordSymbol of chordSymbols) {
|
||||||
|
// Get the chord notes from mode-specific chords data (in C key)
|
||||||
|
const chordNotes = modeChords[chordSymbol];
|
||||||
|
if (!chordNotes) {
|
||||||
|
console.warn(`Chord symbol not found in mode ${modeId} chords: ${chordSymbol}`);
|
||||||
|
continue; // Skip this chord if not found
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the key is C, use notes as-is; otherwise transpose
|
||||||
|
if (rootNote === 'C') {
|
||||||
|
chordMap[chordSymbol] = chordNotes;
|
||||||
|
} else {
|
||||||
|
// Transpose each note in the chord
|
||||||
|
const transposedNotes = chordNotes.map(note => transposeNote(note, transposeSemitones));
|
||||||
|
chordMap[chordSymbol] = transposedNotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chordMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the transposed notes for a specific chord in a given key and mode
|
||||||
|
* @param chordSymbol - Chord symbol (e.g., "I", "V7", "ii")
|
||||||
|
* @param keySignature - Key signature like "C major", "F# minor"
|
||||||
|
* @param modeId - Mode ID (e.g., "ionian", "aeolian")
|
||||||
|
* @returns Array of note names for the chord in the specified key, or empty array if not found
|
||||||
|
*/
|
||||||
|
export const getChordNotesInKey = (
|
||||||
|
chordSymbol: string,
|
||||||
|
keySignature: KeySignature,
|
||||||
|
modeId: string
|
||||||
|
): string[] => {
|
||||||
|
// Get the functional chords for this mode
|
||||||
|
const functionalChords = KGCore.FUNCTIONAL_CHORDS_DATA[modeId];
|
||||||
|
if (!functionalChords) {
|
||||||
|
console.warn(`No functional chords found for mode: ${modeId}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mode-specific chords data
|
||||||
|
const modeChords = functionalChords.chords;
|
||||||
|
if (!modeChords) {
|
||||||
|
console.warn(`No chords data found for mode: ${modeId}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the root note from key signature
|
||||||
|
const rootNote = getRootNoteFromKeySignature(keySignature);
|
||||||
|
|
||||||
|
// Get the chord notes from mode-specific chords data (in C key)
|
||||||
|
const chordNotes = modeChords[chordSymbol];
|
||||||
|
if (!chordNotes) {
|
||||||
|
console.warn(`Chord symbol not found in mode ${modeId} chords: ${chordSymbol}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the key is C, return the notes as-is
|
||||||
|
if (rootNote === 'C') {
|
||||||
|
return chordNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the transposition interval from C to the root note
|
||||||
|
const transposeSemitones = noteNameToPitchClass(rootNote);
|
||||||
|
|
||||||
|
// Transpose each note in the chord
|
||||||
|
const transposedNotes = chordNotes.map(note => transposeNote(note, transposeSemitones));
|
||||||
|
|
||||||
|
return transposedNotes;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the CSS background-image string for the piano grid with scale highlighting
|
* Generates the CSS background-image string for the piano grid with scale highlighting
|
||||||
* @param selectedMode - Current mode ID (e.g., "ionian", "dorian")
|
* @param selectedMode - Current mode ID (e.g., "ionian", "dorian")
|
||||||
|
|||||||
Reference in New Issue
Block a user