feat: added logic to get suitable chords (console log only)

This commit is contained in:
Xiaohan-Tian
2025-12-14 21:52:23 -08:00
parent fe9eb79959
commit 8512fb4204
6 changed files with 274 additions and 20 deletions
+34 -13
View File
@@ -12,7 +12,7 @@ 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
@@ -21,12 +21,21 @@ function App() {
// 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(() => {
+61
View File
@@ -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 });
@@ -304,6 +308,61 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} }
}, [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}
/> />
+20 -2
View File
@@ -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">
+4
View File
@@ -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";
+20 -1
View File
@@ -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
View File
@@ -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")