feat: implemented editable automation lane in piano roll window
This commit is contained in:
@@ -11,6 +11,11 @@ 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';
|
||||
import { DeleteMidiEventsCommand } from '../core/commands/note/DeleteMidiEventsCommand';
|
||||
import { KGMidiPitchBend } from '../core/midi/KGMidiPitchBend';
|
||||
import { KGMidiControllerEvent } from '../core/midi/KGMidiControllerEvent';
|
||||
import { getSnappedBeatPosition, getSnappedLength } from '../components/piano-roll/pianoRollSnap';
|
||||
import { useProjectStore } from '../stores/projectStore';
|
||||
|
||||
interface UseNoteOperationsProps {
|
||||
activeRegion: KGMidiRegion | null;
|
||||
@@ -53,79 +58,6 @@ export const useNoteOperations = ({
|
||||
// Get KGCore instance for accessing selected items
|
||||
const core = KGCore.instance();
|
||||
|
||||
// Utility function to calculate snapped beat position
|
||||
const getSnappedBeatPosition = (beatPosition: number, timeSignature: { numerator: number; denominator: number }, useFloorSnapping: boolean = false): number => {
|
||||
const currentSnap = KGPianoRollState.instance().getCurrentSnap();
|
||||
|
||||
// If no snapping is enabled, return the original position
|
||||
if (currentSnap === 'NO SNAP') {
|
||||
return beatPosition;
|
||||
}
|
||||
|
||||
// Parse the snap value (e.g., "1/4", "1/8", "1/16", "1/32")
|
||||
const denominator = parseInt(currentSnap.split('/')[1]);
|
||||
if (isNaN(denominator)) {
|
||||
return beatPosition; // Fallback to no snapping if invalid
|
||||
}
|
||||
|
||||
// Calculate the snap step in beats
|
||||
// In a 4/4 time signature, a quarter note (1/4) is 1 beat
|
||||
// In a 6/8 time signature, an eighth note (1/8) is 1 beat
|
||||
// const { numerator: timeSigNumerator, denominator: timeSigDenominator } = timeSignature;
|
||||
|
||||
// Calculate beats per whole note based on time signature
|
||||
// const beatsPerWholeNote = timeSigNumerator * (4 / timeSigDenominator);
|
||||
|
||||
// Calculate the snap step in beats
|
||||
// snapStep should ALWAYS be 4 / denominator regardless of time signature
|
||||
const snapStep = 4 / denominator;
|
||||
|
||||
// Choose snapping method: floor for note creation, round for dragging
|
||||
const snappedPosition = useFloorSnapping
|
||||
? Math.floor(beatPosition / snapStep) * snapStep
|
||||
: Math.round(beatPosition / snapStep) * snapStep;
|
||||
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log(`Snapping (${useFloorSnapping ? 'floor' : 'round'}): ${beatPosition} -> ${snappedPosition} (snap: ${currentSnap}, step: ${snapStep})`);
|
||||
}
|
||||
|
||||
return snappedPosition;
|
||||
};
|
||||
|
||||
// Utility function to calculate snapped note length
|
||||
const getSnappedLength = (length: number, timeSignature: { numerator: number; denominator: number }): number => {
|
||||
const currentSnap = KGPianoRollState.instance().getCurrentSnap();
|
||||
|
||||
// If no snapping is enabled, return the original length
|
||||
if (currentSnap === 'NO SNAP') {
|
||||
return length;
|
||||
}
|
||||
|
||||
// Parse the snap value (e.g., "1/4", "1/8", "1/16", "1/32")
|
||||
const denominator = parseInt(currentSnap.split('/')[1]);
|
||||
if (isNaN(denominator)) {
|
||||
return length; // Fallback to no snapping if invalid
|
||||
}
|
||||
|
||||
// Calculate the snap step in beats using same formula as position snapping
|
||||
// const { numerator, denominator: timeSigDenominator } = timeSignature;
|
||||
// const beatsPerWholeNote = numerator * (4 / timeSigDenominator);
|
||||
// snapStep should ALWAYS be 4 / denominator regardless of time signature
|
||||
const snapStep = 4 / denominator;
|
||||
|
||||
// Snap length to nearest multiple of snap step
|
||||
const snappedLength = Math.round(length / snapStep) * snapStep;
|
||||
|
||||
// Ensure minimum note length is respected
|
||||
const finalLength = Math.max(PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH, snappedLength);
|
||||
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log(`Length snapping: ${length} -> ${finalLength} (snap: ${currentSnap}, step: ${snapStep})`);
|
||||
}
|
||||
|
||||
return finalLength;
|
||||
};
|
||||
|
||||
// Utility function to delete selected notes from the active region using commands
|
||||
const deleteSelectedNotes = () => {
|
||||
if (!activeRegion) return false;
|
||||
@@ -136,22 +68,36 @@ export const useNoteOperations = ({
|
||||
item instanceof KGMidiNote &&
|
||||
activeRegion.getNotes().some(note => note.getId() === item.getId())
|
||||
) as KGMidiNote[];
|
||||
const selectedPitchBends = selectedItems.filter(item =>
|
||||
item instanceof KGMidiPitchBend &&
|
||||
activeRegion.getPitchBends().some(pitchBend => pitchBend.getId() === item.getId())
|
||||
) as KGMidiPitchBend[];
|
||||
const selectedControllerEvents = selectedItems.filter(item =>
|
||||
item instanceof KGMidiControllerEvent &&
|
||||
activeRegion.getAllControllerEventsFlattened().some(({ event }) => event.getId() === item.getId())
|
||||
) as KGMidiControllerEvent[];
|
||||
|
||||
if (selectedNotes.length === 0) {
|
||||
if (selectedNotes.length === 0 && selectedPitchBends.length === 0 && selectedControllerEvents.length === 0) {
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log('No notes selected for deletion');
|
||||
console.log('No MIDI events selected for deletion');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log(`Deleting ${selectedNotes.length} selected notes using command`);
|
||||
console.log(`Deleting MIDI events: notes=${selectedNotes.length}, pitchBends=${selectedPitchBends.length}, controllers=${selectedControllerEvents.length}`);
|
||||
}
|
||||
|
||||
// Create and execute the delete notes command
|
||||
const noteIds = selectedNotes.map(note => note.getId());
|
||||
const command = new DeleteNotesCommand(noteIds);
|
||||
const pitchBendIds = selectedPitchBends.map(pitchBend => pitchBend.getId());
|
||||
const controllerEventIds = selectedControllerEvents.map(controllerEvent => controllerEvent.getId());
|
||||
const command = pitchBendIds.length > 0 || controllerEventIds.length > 0
|
||||
? new DeleteMidiEventsCommand(noteIds, pitchBendIds, controllerEventIds)
|
||||
: new DeleteNotesCommand(noteIds);
|
||||
core.executeCommand(command);
|
||||
if (pitchBendIds.length > 0 || controllerEventIds.length > 0) {
|
||||
useProjectStore.getState().bumpAutomationRedrawVersion();
|
||||
}
|
||||
|
||||
// Find the track that contains this region and update it for UI sync
|
||||
const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
|
||||
@@ -163,7 +109,7 @@ export const useNoteOperations = ({
|
||||
setNoteUpdateCounter(prev => prev + 1);
|
||||
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log(`Deleted ${selectedNotes.length} notes using DeleteNotesCommand`);
|
||||
console.log('Deleted selected MIDI events using command');
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -189,7 +135,7 @@ export const useNoteOperations = ({
|
||||
const currentSnap = KGPianoRollState.instance().getCurrentSnap();
|
||||
const beatNumber = currentSnap === 'NO SNAP'
|
||||
? Math.floor(rawBeatNumber) // Snap to 1-beat grid when no snapping is selected
|
||||
: getSnappedBeatPosition(rawBeatNumber, timeSignature, true); // Use floor snapping for note creation
|
||||
: getSnappedBeatPosition(rawBeatNumber, currentSnap, true); // Use floor snapping for note creation
|
||||
|
||||
// Calculate the pitch (MIDI note number)
|
||||
// The piano roll is drawn from bottom to top, with higher notes at the top
|
||||
@@ -418,7 +364,11 @@ export const useNoteOperations = ({
|
||||
|
||||
// Apply length snapping to the visual feedback
|
||||
const newLengthInBeats = newWidth / beatWidth;
|
||||
const snappedLengthInBeats = getSnappedLength(newLengthInBeats, timeSignature);
|
||||
const snappedLengthInBeats = getSnappedLength(
|
||||
newLengthInBeats,
|
||||
KGPianoRollState.instance().getCurrentSnap(),
|
||||
PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH,
|
||||
);
|
||||
const snappedWidth = snappedLengthInBeats * beatWidth;
|
||||
|
||||
// Adjust position if necessary for start resize to maintain snapped length
|
||||
@@ -507,7 +457,11 @@ export const useNoteOperations = ({
|
||||
|
||||
// Apply length snapping to the final resize commit
|
||||
const originalLength = newEndBeat - newStartBeat;
|
||||
const snappedLength = getSnappedLength(originalLength, timeSignature);
|
||||
const snappedLength = getSnappedLength(
|
||||
originalLength,
|
||||
KGPianoRollState.instance().getCurrentSnap(),
|
||||
PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH,
|
||||
);
|
||||
|
||||
// Adjust the note bounds to use the snapped length
|
||||
if (resizeEdge === 'end') {
|
||||
@@ -691,7 +645,10 @@ export const useNoteOperations = ({
|
||||
// Apply horizontal snapping based on current snap setting
|
||||
const regionStartBeat = activeRegion.getStartFromBeat();
|
||||
const rawBeatPosition = (rawNewLeft / beatWidth) - regionStartBeat;
|
||||
const snappedBeatPosition = getSnappedBeatPosition(rawBeatPosition, timeSignature);
|
||||
const snappedBeatPosition = getSnappedBeatPosition(
|
||||
rawBeatPosition,
|
||||
KGPianoRollState.instance().getCurrentSnap(),
|
||||
);
|
||||
const snappedLeft = (snappedBeatPosition + regionStartBeat) * beatWidth;
|
||||
|
||||
// Use snapped horizontal position, but keep raw vertical position
|
||||
@@ -894,4 +851,4 @@ export const useNoteOperations = ({
|
||||
handleNoteDragEnd,
|
||||
deleteSelectedNotes
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user