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 { useEffect as useEffectReact, useState, useRef } from 'react';
|
||||
import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool';
|
||||
import { KGPianoRollState } from './core/state/KGPianoRollState';
|
||||
import { KGCore } from './core/KGCore';
|
||||
|
||||
function App() {
|
||||
// Enable global keyboard handler for copy/paste and undo/redo
|
||||
useGlobalKeyboardHandler();
|
||||
|
||||
// Use project store instead of local state for project name and tracks
|
||||
const {
|
||||
const {
|
||||
refreshStatus,
|
||||
loadProject, maxBars, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
||||
loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
||||
showInstrumentSelection
|
||||
} = useProjectStore();
|
||||
|
||||
// Track if app has been initialized to prevent multiple initializations
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Load project when component mounts
|
||||
useEffect(() => {
|
||||
// Guard against multiple initializations (can happen due to React Strict Mode or rerenders)
|
||||
if (hasInitialized.current) {
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
|
||||
const initializeApp = async () => {
|
||||
// Load the current project from KGCore
|
||||
loadProject(null);
|
||||
@@ -34,27 +43,39 @@ function App() {
|
||||
// Initialize store from config after ConfigManager is ready
|
||||
await initializeFromConfig();
|
||||
|
||||
// Load mode list from JSON
|
||||
// Load all mode and chord data files in parallel
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.BASE_URL}resources/modes/mode_list.json`);
|
||||
const data = await response.json();
|
||||
const [modeListResponse, functionalChordsResponse] = await Promise.all([
|
||||
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
|
||||
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) {
|
||||
console.error('Failed to load mode list:', error);
|
||||
// Fallback to default mode
|
||||
KGPianoRollState.MODE_DATA = [{ id: 'ionian', name: 'Ionian', steps: [2, 2, 1, 2, 2, 2, 1] }];
|
||||
console.error('Failed to load mode/chord data:', error);
|
||||
// Fallback to defaults
|
||||
KGCore.MODE_DATA = [{ id: 'ionian', name: 'Ionian', steps: [2, 2, 1, 2, 2, 2, 1] }];
|
||||
KGCore.FUNCTIONAL_CHORDS_DATA = {};
|
||||
}
|
||||
|
||||
// Log maxBars to console
|
||||
console.log(`Project max bars: ${maxBars}`);
|
||||
// Log maxBars after initialization completes
|
||||
const currentMaxBars = useProjectStore.getState().maxBars;
|
||||
console.log(`Project max bars: ${currentMaxBars}`);
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
}, [loadProject, maxBars, initializeFromConfig]);
|
||||
}, [loadProject, initializeFromConfig]);
|
||||
|
||||
// Refresh status periodically to ensure UI is in sync with KGCore
|
||||
useEffect(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
||||
import { ConfigManager } from '../../core/config/ConfigManager';
|
||||
import { beatsToBar } from '../../util/midiUtil';
|
||||
import { UpdateRegionCommand } from '../../core/commands';
|
||||
import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil';
|
||||
|
||||
interface PianoRollProps {
|
||||
onClose: () => void;
|
||||
@@ -39,6 +40,9 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
// Snapping state
|
||||
const [snapping, setSnapping] = useState<string>('NO SNAP');
|
||||
|
||||
// Chord guide state
|
||||
const [chordGuide, setChordGuide] = useState<string>('N');
|
||||
|
||||
// Piano roll state with temporary initial values
|
||||
const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 });
|
||||
|
||||
@@ -303,7 +307,62 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
console.log(`Selected mode: ${value}`);
|
||||
}
|
||||
}, [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
|
||||
const handleSetNoteUpdateTrigger = (setNoteFn: React.Dispatch<React.SetStateAction<number>>) => {
|
||||
triggerNoteUpdateRef.current = setNoteFn;
|
||||
@@ -775,6 +834,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
onSnappingSelect={handleSnappingSelect}
|
||||
selectedMode={selectedMode}
|
||||
onModeChange={handleModeSelect}
|
||||
chordGuide={chordGuide}
|
||||
onChordGuideChange={handleChordGuideSelect}
|
||||
blinkButton={blinkButton}
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { FaMousePointer, FaPencilAlt } from 'react-icons/fa';
|
||||
import { KGDropdown } from '../common';
|
||||
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
|
||||
interface PianoRollToolbarProps {
|
||||
activeTool: 'pointer' | 'pencil';
|
||||
@@ -13,6 +14,8 @@ interface PianoRollToolbarProps {
|
||||
onSnappingSelect: (value: string) => void;
|
||||
selectedMode: string;
|
||||
onModeChange: (value: string) => void;
|
||||
chordGuide: string;
|
||||
onChordGuideChange: (value: string) => void;
|
||||
blinkButton?: string | null;
|
||||
}
|
||||
|
||||
@@ -26,20 +29,35 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
onSnappingSelect,
|
||||
selectedMode,
|
||||
onModeChange,
|
||||
chordGuide,
|
||||
onChordGuideChange,
|
||||
blinkButton = null
|
||||
}) => {
|
||||
return (
|
||||
<div className="piano-roll-toolbar">
|
||||
<div className="toolbar-left">
|
||||
{/* Left section with mode dropdown */}
|
||||
{/* Left section with mode and chord guide dropdowns */}
|
||||
<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}
|
||||
onChange={(value) => onModeChange(value)}
|
||||
label="Mode"
|
||||
buttonClassName="mode-dropdown"
|
||||
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 className="toolbar-center">
|
||||
|
||||
@@ -17,6 +17,10 @@ export class KGCore {
|
||||
// Private static instance for singleton pattern
|
||||
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 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 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 MODE_DATA: Array<{ id: string; name: string; steps: number[] }> = []; // Modes with id, display name, and interval steps
|
||||
|
||||
private activeTool: string = "pointer";
|
||||
private currentSnap: string = "NO SNAP";
|
||||
private lastEditedNoteLength: number = 1; // Default to 1 beat
|
||||
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() {
|
||||
console.log("KGPianoRollState initialized");
|
||||
}
|
||||
@@ -58,4 +61,20 @@ export class KGPianoRollState {
|
||||
public setCurrentMode(mode: string): void {
|
||||
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 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
|
||||
*/
|
||||
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) {
|
||||
console.warn(`Mode not found: ${modeId}, defaulting to ionian`);
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param selectedMode - Current mode ID (e.g., "ionian", "dorian")
|
||||
|
||||
Reference in New Issue
Block a user