feat: implemented the logic to automatically create a chord when a suitable guide chord is highlighted.

This commit is contained in:
Xiaohan-Tian
2025-12-15 21:59:56 -08:00
parent fbd13eef9f
commit 54b227c1be
3 changed files with 100 additions and 16 deletions
+12
View File
@@ -167,6 +167,18 @@ const PianoGrid: React.FC<PianoGridProps> = ({
return highlights; return highlights;
}, [cursorPosition, matchingChords, selectedChordIndex]); }, [cursorPosition, matchingChords, selectedChordIndex]);
const cursorPitch = cursorPosition?.pitch ?? null;
useEffect(() => {
const pianoRollState = KGPianoRollState.instance();
pianoRollState.setCurrentMatchingChords(matchingChords);
pianoRollState.setCurrentChordCursorPitch(cursorPitch);
}, [matchingChords, cursorPitch]);
useEffect(() => {
KGPianoRollState.instance().setCurrentSelectedChordIndex(selectedChordIndex);
}, [selectedChordIndex]);
const lastEditedNoteLength = KGPianoRollState.instance().getLastEditedNoteLength(); const lastEditedNoteLength = KGPianoRollState.instance().getLastEditedNoteLength();
// Reset selected chord index when cursor moves to a different pitch or beat // Reset selected chord index when cursor moves to a different pitch or beat
+27
View File
@@ -17,6 +17,9 @@ export class KGPianoRollState {
// Chord guide state // Chord guide state
private currentSuitableChords: Record<string, string[]> = {}; // Map of chord symbols to note names (e.g., {"I": ["C", "E", "G"]}) 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 currentSuitableChordsPitchClasses: Record<string, number[]> = {}; // Map of chord symbols to pitch classes (e.g., {"I": [0, 4, 7]})
private currentMatchingChords: number[][] = [];
private currentSelectedChordIndex: number = 0;
private currentChordCursorPitch: number | null = null;
private constructor() { private constructor() {
console.log("KGPianoRollState initialized"); console.log("KGPianoRollState initialized");
@@ -77,4 +80,28 @@ export class KGPianoRollState {
public setCurrentSuitableChordsPitchClasses(chordsPitchClasses: Record<string, number[]>): void { public setCurrentSuitableChordsPitchClasses(chordsPitchClasses: Record<string, number[]>): void {
this.currentSuitableChordsPitchClasses = chordsPitchClasses; this.currentSuitableChordsPitchClasses = chordsPitchClasses;
} }
public getCurrentMatchingChords(): number[][] {
return this.currentMatchingChords;
}
public setCurrentMatchingChords(chords: number[][]): void {
this.currentMatchingChords = chords;
}
public getCurrentSelectedChordIndex(): number {
return this.currentSelectedChordIndex;
}
public setCurrentSelectedChordIndex(index: number): void {
this.currentSelectedChordIndex = index;
}
public getCurrentChordCursorPitch(): number | null {
return this.currentChordCursorPitch;
}
public setCurrentChordCursorPitch(pitch: number | null): void {
this.currentChordCursorPitch = pitch;
}
} }
+57 -12
View File
@@ -10,6 +10,7 @@ import { KGCore } from '../core/KGCore';
import { KGPianoRollState } from '../core/state/KGPianoRollState'; import { KGPianoRollState } from '../core/state/KGPianoRollState';
import { KGAudioInterface } from '../core/audio-interface/KGAudioInterface'; import { KGAudioInterface } from '../core/audio-interface/KGAudioInterface';
import { CreateNoteCommand, DeleteNotesCommand, ResizeNotesCommand, MoveNotesCommand } from '../core/commands'; import { CreateNoteCommand, DeleteNotesCommand, ResizeNotesCommand, MoveNotesCommand } from '../core/commands';
import { CreateNotesCommand } from '../core/commands/note/CreateNotesCommand';
interface UseNoteOperationsProps { interface UseNoteOperationsProps {
activeRegion: KGMidiRegion | null; activeRegion: KGMidiRegion | null;
@@ -210,20 +211,64 @@ export const useNoteOperations = ({
const lastEditedLength = KGPianoRollState.instance().getLastEditedNoteLength(); const lastEditedLength = KGPianoRollState.instance().getLastEditedNoteLength();
const noteEndBeat = noteStartBeat + lastEditedLength; // Use last edited note length const noteEndBeat = noteStartBeat + lastEditedLength; // Use last edited note length
const velocity = 127; // Maximum velocity const velocity = 127; // Maximum velocity
const pianoRollState = KGPianoRollState.instance();
const matchingChordPitches = pianoRollState.getCurrentMatchingChords();
const selectedChordIndex = pianoRollState.getCurrentSelectedChordIndex();
const cursorChordPitch = pianoRollState.getCurrentChordCursorPitch();
const chordIndex = matchingChordPitches.length > 0
? ((selectedChordIndex % matchingChordPitches.length) + matchingChordPitches.length) % matchingChordPitches.length
: 0;
// Create and execute the note creation command let chordNotePitches: number[] = [];
const command = new CreateNoteCommand(
if (
matchingChordPitches.length > 0 &&
cursorChordPitch !== null &&
Math.abs(cursorChordPitch - pitch) <= 1
) {
const cursorOctave = Math.floor(cursorChordPitch / 12);
const pitchClasses = matchingChordPitches[chordIndex] || [];
chordNotePitches = pitchClasses
.map(actualPitchClass => {
const actualPitch = cursorOctave * 12 + actualPitchClass;
return actualPitch >= 0 && actualPitch <= 127 ? actualPitch : null;
})
.filter((value): value is number => value !== null);
chordNotePitches = Array.from(new Set(chordNotePitches));
}
const notePitchesToCreate = chordNotePitches.length > 0 ? chordNotePitches : [pitch];
const isChordCreation = notePitchesToCreate.length > 1;
let createdNotes: KGMidiNote[] = [];
if (isChordCreation) {
const chordCommand = new CreateNotesCommand(
notePitchesToCreate.map(notePitch => ({
regionId: activeRegion.getId(),
startBeat: noteStartBeat,
endBeat: noteEndBeat,
pitch: notePitch,
velocity
}))
);
core.executeCommand(chordCommand);
createdNotes = chordCommand.getCreatedNotes().map(({ note }) => note);
} else {
const singleNoteCommand = new CreateNoteCommand(
activeRegion.getId(), activeRegion.getId(),
noteStartBeat, noteStartBeat,
noteEndBeat, noteEndBeat,
pitch, notePitchesToCreate[0],
velocity velocity
); );
core.executeCommand(singleNoteCommand);
core.executeCommand(command); const createdNote = singleNoteCommand.getCreatedNote();
if (createdNote) {
// Get the created note for audio preview createdNotes = [createdNote];
const createdNote = command.getCreatedNote(); }
}
// Increment the note update counter to trigger a re-render // Increment the note update counter to trigger a re-render
setNoteUpdateCounter((prev: number) => prev + 1); setNoteUpdateCounter((prev: number) => prev + 1);
@@ -238,19 +283,19 @@ export const useNoteOperations = ({
updateTrack(track); updateTrack(track);
// Play note preview if audio interface is ready and note was created // Play note preview if audio interface is ready and note was created
if (createdNote) { if (createdNotes.length > 0) {
const audioInterface = KGAudioInterface.instance(); const audioInterface = KGAudioInterface.instance();
if (audioInterface.getIsInitialized()) { if (audioInterface.getIsInitialized()) {
// Try to start audio context if not started yet (user interaction will allow this)
if (!audioInterface.getIsAudioContextStarted()) { if (!audioInterface.getIsAudioContextStarted()) {
audioInterface.startAudioContext().catch(() => { audioInterface.startAudioContext().catch(() => {
// Silently fail if still not allowed - browser policy // Silently fail if still not allowed - browser policy
}); });
} }
// Trigger note if audio context is now started
if (audioInterface.getIsAudioContextStarted()) { if (audioInterface.getIsAudioContextStarted()) {
audioInterface.triggerNote(track.getId().toString(), createdNote); createdNotes.forEach(note => {
audioInterface.triggerNote(track.getId().toString(), note);
});
} }
} }
} }