feat: implemented the logic to automatically create a chord when a suitable guide chord is highlighted.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user