diff --git a/src/components/piano-roll/PianoGrid.tsx b/src/components/piano-roll/PianoGrid.tsx index 142386a..14e833c 100644 --- a/src/components/piano-roll/PianoGrid.tsx +++ b/src/components/piano-roll/PianoGrid.tsx @@ -167,6 +167,18 @@ const PianoGrid: React.FC = ({ return highlights; }, [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(); // Reset selected chord index when cursor moves to a different pitch or beat diff --git a/src/core/state/KGPianoRollState.ts b/src/core/state/KGPianoRollState.ts index c2ca883..bcef9b9 100644 --- a/src/core/state/KGPianoRollState.ts +++ b/src/core/state/KGPianoRollState.ts @@ -17,6 +17,9 @@ export class KGPianoRollState { // Chord guide state private currentSuitableChords: Record = {}; // Map of chord symbols to note names (e.g., {"I": ["C", "E", "G"]}) private currentSuitableChordsPitchClasses: Record = {}; // 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() { console.log("KGPianoRollState initialized"); @@ -77,4 +80,28 @@ export class KGPianoRollState { public setCurrentSuitableChordsPitchClasses(chordsPitchClasses: Record): void { 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; + } } \ No newline at end of file diff --git a/src/hooks/useNoteOperations.ts b/src/hooks/useNoteOperations.ts index 9b398ca..3e29bde 100644 --- a/src/hooks/useNoteOperations.ts +++ b/src/hooks/useNoteOperations.ts @@ -10,6 +10,7 @@ import { KGCore } from '../core/KGCore'; import { KGPianoRollState } from '../core/state/KGPianoRollState'; import { KGAudioInterface } from '../core/audio-interface/KGAudioInterface'; import { CreateNoteCommand, DeleteNotesCommand, ResizeNotesCommand, MoveNotesCommand } from '../core/commands'; +import { CreateNotesCommand } from '../core/commands/note/CreateNotesCommand'; interface UseNoteOperationsProps { activeRegion: KGMidiRegion | null; @@ -210,20 +211,64 @@ export const useNoteOperations = ({ const lastEditedLength = KGPianoRollState.instance().getLastEditedNoteLength(); const noteEndBeat = noteStartBeat + lastEditedLength; // Use last edited note length 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 - const command = new CreateNoteCommand( - activeRegion.getId(), - noteStartBeat, - noteEndBeat, - pitch, - velocity - ); + let chordNotePitches: number[] = []; - core.executeCommand(command); + 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); - // Get the created note for audio preview - const createdNote = command.getCreatedNote(); + 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(), + noteStartBeat, + noteEndBeat, + notePitchesToCreate[0], + velocity + ); + core.executeCommand(singleNoteCommand); + const createdNote = singleNoteCommand.getCreatedNote(); + if (createdNote) { + createdNotes = [createdNote]; + } + } // Increment the note update counter to trigger a re-render setNoteUpdateCounter((prev: number) => prev + 1); @@ -238,19 +283,19 @@ export const useNoteOperations = ({ updateTrack(track); // Play note preview if audio interface is ready and note was created - if (createdNote) { + if (createdNotes.length > 0) { const audioInterface = KGAudioInterface.instance(); if (audioInterface.getIsInitialized()) { - // Try to start audio context if not started yet (user interaction will allow this) if (!audioInterface.getIsAudioContextStarted()) { audioInterface.startAudioContext().catch(() => { // Silently fail if still not allowed - browser policy }); } - - // Trigger note if audio context is now started + if (audioInterface.getIsAudioContextStarted()) { - audioInterface.triggerNote(track.getId().toString(), createdNote); + createdNotes.forEach(note => { + audioInterface.triggerNote(track.getId().toString(), note); + }); } } }