feat: added pitch bend support
This commit is contained in:
@@ -35,6 +35,8 @@ export { ResizeNotesCommand } from './note/ResizeNotesCommand';
|
||||
export { MoveNotesCommand } from './note/MoveNotesCommand';
|
||||
export { PasteNotesCommand } from './note/PasteNotesCommand';
|
||||
export { UpdateNotePropertiesCommand } from './note/UpdateNotePropertiesCommand';
|
||||
export { UpdatePitchBendPropertiesCommand } from './note/UpdatePitchBendPropertiesCommand';
|
||||
export { CreateMidiEventsCommand, type PitchBendCreationData, type NoteCreationData } from './note/CreateMidiEventsCommand';
|
||||
|
||||
// Project commands
|
||||
export { ChangeProjectPropertyCommand, type ProjectUpdateProperties } from './project/ChangeProjectPropertyCommand';
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
|
||||
export interface NoteCreationData {
|
||||
regionId: string;
|
||||
startBeat: number;
|
||||
endBeat: number;
|
||||
pitch: number;
|
||||
velocity: number;
|
||||
noteId?: string;
|
||||
}
|
||||
|
||||
export interface PitchBendCreationData {
|
||||
regionId: string;
|
||||
beat: number;
|
||||
value: number;
|
||||
pitchBendId?: string;
|
||||
}
|
||||
|
||||
export class CreateMidiEventsCommand extends KGCommand {
|
||||
private noteCreationData: NoteCreationData[];
|
||||
private pitchBendCreationData: PitchBendCreationData[];
|
||||
private createdNotes: Array<{ note: KGMidiNote; regionId: string }> = [];
|
||||
private createdPitchBends: Array<{ pitchBend: KGMidiPitchBend; regionId: string }> = [];
|
||||
|
||||
constructor(noteCreationData: NoteCreationData[], pitchBendCreationData: PitchBendCreationData[] = []) {
|
||||
super();
|
||||
this.noteCreationData = noteCreationData.map(data => ({
|
||||
...data,
|
||||
noteId: data.noteId || generateUniqueId('KGMidiNote'),
|
||||
}));
|
||||
this.pitchBendCreationData = pitchBendCreationData.map(data => ({
|
||||
...data,
|
||||
pitchBendId: data.pitchBendId || generateUniqueId('KGMidiPitchBend'),
|
||||
}));
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tracks = KGCore.instance().getCurrentProject().getTracks();
|
||||
this.createdNotes = [];
|
||||
this.createdPitchBends = [];
|
||||
|
||||
for (const noteData of this.noteCreationData) {
|
||||
const targetRegion = this.resolveRegion(tracks, noteData.regionId);
|
||||
const newNote = new KGMidiNote(
|
||||
noteData.noteId!,
|
||||
noteData.startBeat,
|
||||
noteData.endBeat,
|
||||
noteData.pitch,
|
||||
noteData.velocity
|
||||
);
|
||||
targetRegion.addNote(newNote);
|
||||
this.createdNotes.push({ note: newNote, regionId: noteData.regionId });
|
||||
}
|
||||
|
||||
for (const pitchBendData of this.pitchBendCreationData) {
|
||||
const targetRegion = this.resolveRegion(tracks, pitchBendData.regionId);
|
||||
const newPitchBend = new KGMidiPitchBend(
|
||||
pitchBendData.pitchBendId!,
|
||||
pitchBendData.beat,
|
||||
pitchBendData.value
|
||||
);
|
||||
targetRegion.addPitchBend(newPitchBend);
|
||||
this.createdPitchBends.push({ pitchBend: newPitchBend, regionId: pitchBendData.regionId });
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const core = KGCore.instance();
|
||||
const tracks = core.getCurrentProject().getTracks();
|
||||
|
||||
for (const data of this.createdNotes) {
|
||||
const region = this.resolveRegion(tracks, data.regionId);
|
||||
region.removeNote(data.note.getId());
|
||||
const selectedNote = core.getSelectedItems().find(item => item instanceof KGMidiNote && item.getId() === data.note.getId());
|
||||
if (selectedNote) {
|
||||
core.removeSelectedItem(selectedNote);
|
||||
}
|
||||
}
|
||||
|
||||
for (const data of this.createdPitchBends) {
|
||||
const region = this.resolveRegion(tracks, data.regionId);
|
||||
region.removePitchBend(data.pitchBend.getId());
|
||||
const selectedPitchBend = core.getSelectedItems().find(item => item instanceof KGMidiPitchBend && item.getId() === data.pitchBend.getId());
|
||||
if (selectedPitchBend) {
|
||||
core.removeSelectedItem(selectedPitchBend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteCount = this.noteCreationData.length;
|
||||
const pitchBendCount = this.pitchBendCreationData.length;
|
||||
|
||||
if (noteCount > 0 && pitchBendCount > 0) {
|
||||
return `Create ${noteCount} note${noteCount === 1 ? '' : 's'} and ${pitchBendCount} pitch bend${pitchBendCount === 1 ? '' : 's'}`;
|
||||
}
|
||||
if (pitchBendCount > 0) {
|
||||
return pitchBendCount === 1 ? 'Create pitch bend' : `Create ${pitchBendCount} pitch bends`;
|
||||
}
|
||||
return noteCount === 1 ? 'Create note' : `Create ${noteCount} notes`;
|
||||
}
|
||||
|
||||
public getNoteCreationData(): NoteCreationData[] {
|
||||
return this.noteCreationData;
|
||||
}
|
||||
|
||||
public getCreatedNotes(): Array<{ note: KGMidiNote; regionId: string }> {
|
||||
return this.createdNotes;
|
||||
}
|
||||
|
||||
public getCreatedNoteIds(): string[] {
|
||||
return this.noteCreationData.map(data => data.noteId!);
|
||||
}
|
||||
|
||||
private resolveRegion(tracks: KGTrack[], regionId: string): KGMidiRegion {
|
||||
for (const track of tracks) {
|
||||
const region = track.getRegions().find(candidate => candidate.getId() === regionId);
|
||||
if (region instanceof KGMidiRegion) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`MIDI region with ID ${regionId} not found`);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +1,28 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
import { CreateMidiEventsCommand, type NoteCreationData } from './CreateMidiEventsCommand';
|
||||
|
||||
/**
|
||||
* Data structure for a note to be created
|
||||
*/
|
||||
export interface NoteCreationData {
|
||||
regionId: string;
|
||||
startBeat: number;
|
||||
endBeat: number;
|
||||
pitch: number;
|
||||
velocity: number;
|
||||
noteId?: string;
|
||||
}
|
||||
export type { NoteCreationData } from './CreateMidiEventsCommand';
|
||||
|
||||
/**
|
||||
* Command to create multiple MIDI notes in regions
|
||||
* Handles bulk creation as a single undoable operation
|
||||
*/
|
||||
export class CreateNotesCommand extends KGCommand {
|
||||
private noteCreationData: NoteCreationData[];
|
||||
private createdNotes: Array<{
|
||||
note: KGMidiNote;
|
||||
regionId: string;
|
||||
}> = [];
|
||||
|
||||
export class CreateNotesCommand extends CreateMidiEventsCommand {
|
||||
constructor(noteCreationData: NoteCreationData[]) {
|
||||
super();
|
||||
this.noteCreationData = noteCreationData.map(data => ({
|
||||
...data,
|
||||
noteId: data.noteId || generateUniqueId('KGMidiNote')
|
||||
}));
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Clear any existing created note data to prevent duplicates on re-execution
|
||||
this.createdNotes = [];
|
||||
|
||||
// Create all notes
|
||||
for (const noteData of this.noteCreationData) {
|
||||
// Find the target region
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === noteData.regionId);
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
targetRegion = region;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion) {
|
||||
throw new Error(`MIDI region with ID ${noteData.regionId} not found`);
|
||||
}
|
||||
|
||||
// Create the new MIDI note
|
||||
const newNote = new KGMidiNote(
|
||||
noteData.noteId!,
|
||||
noteData.startBeat,
|
||||
noteData.endBeat,
|
||||
noteData.pitch,
|
||||
noteData.velocity
|
||||
);
|
||||
|
||||
// Add the note to the region
|
||||
targetRegion.addNote(newNote);
|
||||
|
||||
// Store for undo
|
||||
this.createdNotes.push({
|
||||
note: newNote,
|
||||
regionId: noteData.regionId
|
||||
});
|
||||
}
|
||||
|
||||
const noteCount = this.createdNotes.length;
|
||||
const regionCount = new Set(this.createdNotes.map(data => data.regionId)).size;
|
||||
console.log(`Created ${noteCount} notes in ${regionCount} region${regionCount > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.createdNotes.length === 0) {
|
||||
throw new Error('Cannot undo: no notes were created');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Remove all created notes from their regions
|
||||
for (const data of this.createdNotes) {
|
||||
// Find the region
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === data.regionId);
|
||||
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
region.removeNote(data.note.getId());
|
||||
|
||||
// Clear selection if this note was selected
|
||||
const selectedItems = core.getSelectedItems();
|
||||
const selectedNote = selectedItems.find(item =>
|
||||
item instanceof KGMidiNote && item.getId() === data.note.getId()
|
||||
);
|
||||
if (selectedNote) {
|
||||
core.removeSelectedItem(selectedNote);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Removed ${this.createdNotes.length} created notes from ${new Set(this.createdNotes.map(d => d.regionId)).size} regions`);
|
||||
super(noteCreationData, []);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.noteCreationData.length === 1) {
|
||||
const noteData = this.noteCreationData[0];
|
||||
const noteCreationData = this.getNoteCreationData();
|
||||
if (noteCreationData.length === 1) {
|
||||
const noteData = noteCreationData[0];
|
||||
// Convert MIDI pitch to note name for user-friendly description
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(noteData.pitch / 12) - 1;
|
||||
const noteName = noteNames[noteData.pitch % 12];
|
||||
return `Create note ${noteName}${octave}`;
|
||||
}
|
||||
return `Create ${this.noteCreationData.length} notes`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note creation data that was/will be processed
|
||||
*/
|
||||
public getNoteCreationData(): NoteCreationData[] {
|
||||
return this.noteCreationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created note instances (only available after execute)
|
||||
*/
|
||||
public getCreatedNotes(): Array<{note: KGMidiNote; regionId: string}> {
|
||||
return this.createdNotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the regions that were affected by this creation
|
||||
*/
|
||||
public getAffectedRegionIds(): string[] {
|
||||
return Array.from(new Set(this.noteCreationData.map(data => data.regionId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IDs of notes that were/will be created
|
||||
*/
|
||||
public getCreatedNoteIds(): string[] {
|
||||
return this.noteCreationData.map(data => data.noteId!);
|
||||
return `Create ${noteCreationData.length} notes`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,4 +119,4 @@ export class CreateNoteCommand extends CreateNotesCommand {
|
||||
velocity
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
|
||||
interface PitchBendSnapshot {
|
||||
pitchBendId: string;
|
||||
beat: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface PitchBendUpdate {
|
||||
pitchBendId: string;
|
||||
beat?: number;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export class UpdatePitchBendPropertiesCommand extends KGCommand {
|
||||
private regionId: string;
|
||||
private snapshots: PitchBendSnapshot[];
|
||||
private updates: PitchBendUpdate[];
|
||||
private targetRegion: KGMidiRegion | null = null;
|
||||
private parentTrack: KGTrack | null = null;
|
||||
|
||||
constructor(regionId: string, snapshots: PitchBendSnapshot[], updates: PitchBendUpdate[]) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
this.snapshots = [...snapshots];
|
||||
this.updates = [...updates];
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tracks = KGCore.instance().getCurrentProject().getTracks();
|
||||
|
||||
for (const track of tracks) {
|
||||
const region = track.getRegions().find(r => r.getId() === this.regionId) as KGMidiRegion | undefined;
|
||||
if (region) {
|
||||
this.targetRegion = region;
|
||||
this.parentTrack = track;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.targetRegion) {
|
||||
throw new Error(`Region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
const pitchBends = this.targetRegion.getPitchBends();
|
||||
for (const update of this.updates) {
|
||||
const pitchBend = pitchBends.find(candidate => candidate.getId() === update.pitchBendId);
|
||||
if (pitchBend) {
|
||||
if (update.beat !== undefined) pitchBend.setBeat(update.beat);
|
||||
if (update.value !== undefined) pitchBend.setValue(update.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetRegion) {
|
||||
throw new Error('Cannot undo: command was not executed');
|
||||
}
|
||||
|
||||
const pitchBends = this.targetRegion.getPitchBends();
|
||||
this.snapshots.forEach(snapshot => {
|
||||
const pitchBend = pitchBends.find(candidate => candidate.getId() === snapshot.pitchBendId);
|
||||
if (pitchBend) {
|
||||
pitchBend.setBeat(snapshot.beat);
|
||||
pitchBend.setValue(snapshot.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const count = this.snapshots.length;
|
||||
return count === 1 ? 'Update pitch bend properties' : `Update ${count} pitch bends' properties`;
|
||||
}
|
||||
|
||||
public getParentTrack(): KGTrack | null {
|
||||
return this.parentTrack;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
import { useProjectStore } from '../../../stores/projectStore';
|
||||
|
||||
@@ -16,6 +17,11 @@ interface RegionSnapshot {
|
||||
pitch: number;
|
||||
velocity: number;
|
||||
}>;
|
||||
pitchBends: Array<{
|
||||
id: string;
|
||||
beat: number;
|
||||
value: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ResolvedRegion {
|
||||
@@ -33,6 +39,14 @@ function cloneNote(note: KGMidiNote, startBeat: number, endBeat: number): KGMidi
|
||||
);
|
||||
}
|
||||
|
||||
function clonePitchBend(pitchBend: KGMidiPitchBend, beat: number): KGMidiPitchBend {
|
||||
return new KGMidiPitchBend(
|
||||
pitchBend.getId(),
|
||||
beat,
|
||||
pitchBend.getValue()
|
||||
);
|
||||
}
|
||||
|
||||
export class MergeMidiRegionsCommand extends KGCommand {
|
||||
private readonly regionIdsToMerge: string[];
|
||||
private targetTrack: KGTrack | null = null;
|
||||
@@ -102,6 +116,11 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
pitch: note.getPitch(),
|
||||
velocity: note.getVelocity(),
|
||||
})),
|
||||
pitchBends: region.getPitchBends().map(pitchBend => ({
|
||||
id: pitchBend.getId(),
|
||||
beat: pitchBend.getBeat(),
|
||||
value: pitchBend.getValue(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,6 +135,7 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
), survivingRegionStart + this.survivingRegion.getLength());
|
||||
|
||||
const mergedNotes = [...this.survivingRegion.getNotes()];
|
||||
const mergedPitchBends = [...this.survivingRegion.getPitchBends()];
|
||||
for (const { region } of resolvedRegions.slice(1)) {
|
||||
const regionStart = region.getStartFromBeat();
|
||||
region.getNotes().forEach(note => {
|
||||
@@ -127,10 +147,17 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
absoluteEnd - survivingRegionStart
|
||||
));
|
||||
});
|
||||
region.getPitchBends().forEach(pitchBend => {
|
||||
mergedPitchBends.push(clonePitchBend(
|
||||
pitchBend,
|
||||
regionStart + pitchBend.getBeat() - survivingRegionStart
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
this.survivingRegion.setLength(mergedEndBeat - survivingRegionStart);
|
||||
this.survivingRegion.setNotes(mergedNotes);
|
||||
this.survivingRegion.setPitchBends(mergedPitchBends);
|
||||
|
||||
const removedRegionIds = new Set(this.removedRegions.map(({ region }) => region.getId()));
|
||||
const nextRegions = resolvedTargetTrack.getRegions().filter(region => !removedRegionIds.has(region.getId()));
|
||||
@@ -165,6 +192,11 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
note.pitch,
|
||||
note.velocity
|
||||
)));
|
||||
this.survivingRegion.setPitchBends(survivingSnapshot.pitchBends.map(pitchBend => new KGMidiPitchBend(
|
||||
pitchBend.id,
|
||||
pitchBend.beat,
|
||||
pitchBend.value
|
||||
)));
|
||||
|
||||
for (const { region } of this.removedRegions) {
|
||||
const snapshot = this.originalRegionSnapshots.get(region.getId());
|
||||
@@ -180,6 +212,11 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
note.pitch,
|
||||
note.velocity
|
||||
)));
|
||||
region.setPitchBends(snapshot.pitchBends.map(pitchBend => new KGMidiPitchBend(
|
||||
pitchBend.id,
|
||||
pitchBend.beat,
|
||||
pitchBend.value
|
||||
)));
|
||||
}
|
||||
|
||||
const regions = [...this.targetTrack.getRegions()];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { KGCore } from '../../KGCore';
|
||||
import { KGRegion } from '../../region/KGRegion';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
import { useProjectStore } from '../../../stores/projectStore';
|
||||
@@ -82,6 +83,13 @@ export class PasteRegionsCommand extends KGCommand {
|
||||
);
|
||||
(newRegion as KGMidiRegion).addNote(copiedNote);
|
||||
});
|
||||
originalRegion.getPitchBends().forEach(pitchBend => {
|
||||
(newRegion as KGMidiRegion).addPitchBend(new KGMidiPitchBend(
|
||||
generateUniqueId('KGMidiPitchBend'),
|
||||
pitchBend.getBeat(),
|
||||
pitchBend.getValue()
|
||||
));
|
||||
});
|
||||
|
||||
console.log(`Created MIDI region "${newRegion.getName()}" with ${originalNotes.length} notes`);
|
||||
} else {
|
||||
@@ -203,4 +211,4 @@ export class PasteRegionsCommand extends KGCommand {
|
||||
public static fromRegions(targetTrackId: string, pastePosition: number, regions: KGRegion[]): PasteRegionsCommand {
|
||||
return new PasteRegionsCommand(targetTrackId, pastePosition, regions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { KGRegion } from '../../region/KGRegion';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../region/KGAudioRegion';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
|
||||
/**
|
||||
* Command to resize a region (change start position and/or length)
|
||||
@@ -23,6 +24,10 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
originalStartBeat: number;
|
||||
originalEndBeat: number;
|
||||
}> = [];
|
||||
private pitchBendAdjustments: Array<{
|
||||
pitchBendId: string;
|
||||
originalBeat: number;
|
||||
}> = [];
|
||||
|
||||
// Audio region clip offset support
|
||||
private newClipStartOffsetSeconds?: number;
|
||||
@@ -81,6 +86,13 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
note.setStartBeat(note.getStartBeat() - beatOffset);
|
||||
note.setEndBeat(note.getEndBeat() - beatOffset);
|
||||
});
|
||||
targetRegion.getPitchBends().forEach((pitchBend: KGMidiPitchBend) => {
|
||||
this.pitchBendAdjustments.push({
|
||||
pitchBendId: pitchBend.getId(),
|
||||
originalBeat: pitchBend.getBeat(),
|
||||
});
|
||||
pitchBend.setBeat(pitchBend.getBeat() - beatOffset);
|
||||
});
|
||||
|
||||
console.log(`Adjusted ${notes.length} notes by offset ${-beatOffset} beats to maintain absolute positions`);
|
||||
}
|
||||
@@ -122,6 +134,15 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
|
||||
console.log(`Restored ${this.noteAdjustments.length} notes to their original positions`);
|
||||
}
|
||||
if (this.pitchBendAdjustments.length > 0 && this.targetRegion instanceof KGMidiRegion) {
|
||||
const pitchBends = this.targetRegion.getPitchBends();
|
||||
this.pitchBendAdjustments.forEach(adjustment => {
|
||||
const pitchBend = pitchBends.find(candidate => candidate.getId() === adjustment.pitchBendId);
|
||||
if (pitchBend) {
|
||||
pitchBend.setBeat(adjustment.originalBeat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore clip offset for audio regions
|
||||
if (this.targetRegion instanceof KGAudioRegion && this.newClipStartOffsetSeconds !== undefined) {
|
||||
@@ -214,4 +235,4 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
|
||||
return new ResizeRegionCommand(regionId, newStartFromBeat, newLength, newClipStartOffsetSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { KGRegion } from '../../region/KGRegion';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../region/KGAudioRegion';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
import { useProjectStore } from '../../../stores/projectStore';
|
||||
@@ -114,6 +115,22 @@ export class SplitRegionCommand extends KGCommand {
|
||||
}
|
||||
}
|
||||
|
||||
for (const pitchBend of originalRegion.getPitchBends()) {
|
||||
if (pitchBend.getBeat() < splitOffsetBeats) {
|
||||
region1.addPitchBend(new KGMidiPitchBend(
|
||||
generateUniqueId('KGMidiPitchBend'),
|
||||
pitchBend.getBeat(),
|
||||
pitchBend.getValue()
|
||||
));
|
||||
} else {
|
||||
region2.addPitchBend(new KGMidiPitchBend(
|
||||
generateUniqueId('KGMidiPitchBend'),
|
||||
pitchBend.getBeat() - splitOffsetBeats,
|
||||
pitchBend.getValue()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
this.region1 = region1;
|
||||
this.region2 = region2;
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ interface NoteAdjustment {
|
||||
originalEndBeat: number;
|
||||
}
|
||||
|
||||
interface PitchBendAdjustment {
|
||||
pitchBendId: string;
|
||||
originalBeat: number;
|
||||
}
|
||||
|
||||
const EPSILON = 1e-9;
|
||||
|
||||
function getRegionById(tracks: KGTrack[], regionId: string): { region: KGRegion; track: KGTrack } | null {
|
||||
@@ -168,6 +173,7 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
private originalStates: RegionSnapshot[] = [];
|
||||
private targetRegions: KGRegion[] = [];
|
||||
private noteAdjustments = new Map<string, NoteAdjustment[]>();
|
||||
private pitchBendAdjustments = new Map<string, PitchBendAdjustment[]>();
|
||||
|
||||
constructor(
|
||||
primaryRegionId: string,
|
||||
@@ -282,6 +288,13 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
note.setStartBeat(note.getStartBeat() - beatOffset);
|
||||
note.setEndBeat(note.getEndBeat() - beatOffset);
|
||||
});
|
||||
this.pitchBendAdjustments.set(region.getId(), region.getPitchBends().map(pitchBend => ({
|
||||
pitchBendId: pitchBend.getId(),
|
||||
originalBeat: pitchBend.getBeat(),
|
||||
})));
|
||||
region.getPitchBends().forEach(pitchBend => {
|
||||
pitchBend.setBeat(pitchBend.getBeat() - beatOffset);
|
||||
});
|
||||
}
|
||||
|
||||
if (region instanceof KGAudioRegion && projectedState.clipStartOffsetSeconds !== undefined) {
|
||||
@@ -315,6 +328,13 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
note.setEndBeat(adjustment.originalEndBeat);
|
||||
}
|
||||
});
|
||||
const pitchBendAdjustments = this.pitchBendAdjustments.get(region.getId()) ?? [];
|
||||
pitchBendAdjustments.forEach(adjustment => {
|
||||
const pitchBend = region.getPitchBends().find(candidate => candidate.getId() === adjustment.pitchBendId);
|
||||
if (pitchBend) {
|
||||
pitchBend.setBeat(adjustment.originalBeat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (region instanceof KGAudioRegion && originalState.clipStartOffsetSeconds !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user