feat: added MIDI CC recording and list event table operation support.
This commit is contained in:
@@ -37,7 +37,13 @@ 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';
|
||||
export { UpdateControllerEventPropertiesCommand } from './note/UpdateControllerEventPropertiesCommand';
|
||||
export {
|
||||
CreateMidiEventsCommand,
|
||||
type PitchBendCreationData,
|
||||
type NoteCreationData,
|
||||
type ControllerEventCreationData
|
||||
} from './note/CreateMidiEventsCommand';
|
||||
|
||||
// Project commands
|
||||
export { ChangeProjectPropertyCommand, type ProjectUpdateProperties } from './project/ChangeProjectPropertyCommand';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
@@ -22,13 +23,27 @@ export interface PitchBendCreationData {
|
||||
pitchBendId?: string;
|
||||
}
|
||||
|
||||
export interface ControllerEventCreationData {
|
||||
regionId: string;
|
||||
controller: number;
|
||||
beat: number;
|
||||
value: number;
|
||||
controllerEventId?: string;
|
||||
}
|
||||
|
||||
export class CreateMidiEventsCommand extends KGCommand {
|
||||
private noteCreationData: NoteCreationData[];
|
||||
private pitchBendCreationData: PitchBendCreationData[];
|
||||
private controllerEventCreationData: ControllerEventCreationData[];
|
||||
private createdNotes: Array<{ note: KGMidiNote; regionId: string }> = [];
|
||||
private createdPitchBends: Array<{ pitchBend: KGMidiPitchBend; regionId: string }> = [];
|
||||
private createdControllerEvents: Array<{ controller: number; controllerEvent: KGMidiControllerEvent; regionId: string }> = [];
|
||||
|
||||
constructor(noteCreationData: NoteCreationData[], pitchBendCreationData: PitchBendCreationData[] = []) {
|
||||
constructor(
|
||||
noteCreationData: NoteCreationData[],
|
||||
pitchBendCreationData: PitchBendCreationData[] = [],
|
||||
controllerEventCreationData: ControllerEventCreationData[] = []
|
||||
) {
|
||||
super();
|
||||
this.noteCreationData = noteCreationData.map(data => ({
|
||||
...data,
|
||||
@@ -38,12 +53,17 @@ export class CreateMidiEventsCommand extends KGCommand {
|
||||
...data,
|
||||
pitchBendId: data.pitchBendId || generateUniqueId('KGMidiPitchBend'),
|
||||
}));
|
||||
this.controllerEventCreationData = controllerEventCreationData.map(data => ({
|
||||
...data,
|
||||
controllerEventId: data.controllerEventId || generateUniqueId('KGMidiControllerEvent'),
|
||||
}));
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tracks = KGCore.instance().getCurrentProject().getTracks();
|
||||
this.createdNotes = [];
|
||||
this.createdPitchBends = [];
|
||||
this.createdControllerEvents = [];
|
||||
|
||||
for (const noteData of this.noteCreationData) {
|
||||
const targetRegion = this.resolveRegion(tracks, noteData.regionId);
|
||||
@@ -68,6 +88,21 @@ export class CreateMidiEventsCommand extends KGCommand {
|
||||
targetRegion.addPitchBend(newPitchBend);
|
||||
this.createdPitchBends.push({ pitchBend: newPitchBend, regionId: pitchBendData.regionId });
|
||||
}
|
||||
|
||||
for (const controllerEventData of this.controllerEventCreationData) {
|
||||
const targetRegion = this.resolveRegion(tracks, controllerEventData.regionId);
|
||||
const newControllerEvent = new KGMidiControllerEvent(
|
||||
controllerEventData.controllerEventId!,
|
||||
controllerEventData.beat,
|
||||
controllerEventData.value
|
||||
);
|
||||
targetRegion.addControllerEvent(controllerEventData.controller, newControllerEvent);
|
||||
this.createdControllerEvents.push({
|
||||
controller: controllerEventData.controller,
|
||||
controllerEvent: newControllerEvent,
|
||||
regionId: controllerEventData.regionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
@@ -91,19 +126,40 @@ export class CreateMidiEventsCommand extends KGCommand {
|
||||
core.removeSelectedItem(selectedPitchBend);
|
||||
}
|
||||
}
|
||||
|
||||
for (const data of this.createdControllerEvents) {
|
||||
const region = this.resolveRegion(tracks, data.regionId);
|
||||
region.removeControllerEvent(data.controller, data.controllerEvent.getId());
|
||||
const selectedControllerEvent = core.getSelectedItems().find(
|
||||
item => item instanceof KGMidiControllerEvent && item.getId() === data.controllerEvent.getId()
|
||||
);
|
||||
if (selectedControllerEvent) {
|
||||
core.removeSelectedItem(selectedControllerEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteCount = this.noteCreationData.length;
|
||||
const pitchBendCount = this.pitchBendCreationData.length;
|
||||
const controllerEventCount = this.controllerEventCreationData.length;
|
||||
|
||||
if (noteCount > 0 && pitchBendCount > 0) {
|
||||
return `Create ${noteCount} note${noteCount === 1 ? '' : 's'} and ${pitchBendCount} pitch bend${pitchBendCount === 1 ? '' : 's'}`;
|
||||
const parts: string[] = [];
|
||||
if (noteCount > 0) {
|
||||
parts.push(`${noteCount} note${noteCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
if (pitchBendCount > 0) {
|
||||
return pitchBendCount === 1 ? 'Create pitch bend' : `Create ${pitchBendCount} pitch bends`;
|
||||
parts.push(`${pitchBendCount} pitch bend${pitchBendCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
return noteCount === 1 ? 'Create note' : `Create ${noteCount} notes`;
|
||||
if (controllerEventCount > 0) {
|
||||
parts.push(`${controllerEventCount} controller event${controllerEventCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return 'Create MIDI events';
|
||||
}
|
||||
|
||||
return `Create ${parts.join(' and ')}`;
|
||||
}
|
||||
|
||||
public getNoteCreationData(): NoteCreationData[] {
|
||||
@@ -118,6 +174,10 @@ export class CreateMidiEventsCommand extends KGCommand {
|
||||
return this.createdPitchBends;
|
||||
}
|
||||
|
||||
public getCreatedControllerEvents(): Array<{ controller: number; controllerEvent: KGMidiControllerEvent; regionId: string }> {
|
||||
return this.createdControllerEvents;
|
||||
}
|
||||
|
||||
public getCreatedNoteIds(): string[] {
|
||||
return this.noteCreationData.map(data => data.noteId!);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
@@ -16,16 +17,26 @@ interface DeletedPitchBendData {
|
||||
originalIndex: number;
|
||||
}
|
||||
|
||||
interface DeletedControllerEventData {
|
||||
controller: number;
|
||||
controllerEvent: KGMidiControllerEvent;
|
||||
regionId: string;
|
||||
originalIndex: number;
|
||||
}
|
||||
|
||||
export class DeleteMidiEventsCommand extends KGCommand {
|
||||
private noteIds: string[];
|
||||
private pitchBendIds: string[];
|
||||
private controllerEventIds: string[];
|
||||
private deletedNoteData: DeletedNoteData[] = [];
|
||||
private deletedPitchBendData: DeletedPitchBendData[] = [];
|
||||
private deletedControllerEventData: DeletedControllerEventData[] = [];
|
||||
|
||||
constructor(noteIds: string[] = [], pitchBendIds: string[] = []) {
|
||||
constructor(noteIds: string[] = [], pitchBendIds: string[] = [], controllerEventIds: string[] = []) {
|
||||
super();
|
||||
this.noteIds = noteIds;
|
||||
this.pitchBendIds = pitchBendIds;
|
||||
this.controllerEventIds = controllerEventIds;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
@@ -34,6 +45,7 @@ export class DeleteMidiEventsCommand extends KGCommand {
|
||||
|
||||
this.deletedNoteData = [];
|
||||
this.deletedPitchBendData = [];
|
||||
this.deletedControllerEventData = [];
|
||||
|
||||
for (const noteId of this.noteIds) {
|
||||
for (const track of tracks) {
|
||||
@@ -71,12 +83,35 @@ export class DeleteMidiEventsCommand extends KGCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deletedNoteData.length === 0 && this.deletedPitchBendData.length === 0) {
|
||||
for (const controllerEventId of this.controllerEventIds) {
|
||||
for (const track of tracks) {
|
||||
for (const region of track.getRegions()) {
|
||||
if (!(region instanceof KGMidiRegion)) continue;
|
||||
|
||||
const flattened = region.getAllControllerEventsFlattened();
|
||||
const flattenedIndex = flattened.findIndex(({ event }) => event.getId() === controllerEventId);
|
||||
if (flattenedIndex === -1) continue;
|
||||
|
||||
const { controller, event } = flattened[flattenedIndex];
|
||||
const originalIndex = region.getControllerEvents(controller).findIndex(candidate => candidate.getId() === controllerEventId);
|
||||
this.deletedControllerEventData.push({
|
||||
controller,
|
||||
controllerEvent: event,
|
||||
regionId: region.getId(),
|
||||
originalIndex,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deletedNoteData.length === 0 && this.deletedPitchBendData.length === 0 && this.deletedControllerEventData.length === 0) {
|
||||
throw new Error('No MIDI events found to delete');
|
||||
}
|
||||
|
||||
this.deletedNoteData.sort((a, b) => b.originalIndex - a.originalIndex);
|
||||
this.deletedPitchBendData.sort((a, b) => b.originalIndex - a.originalIndex);
|
||||
this.deletedControllerEventData.sort((a, b) => b.originalIndex - a.originalIndex);
|
||||
|
||||
for (const data of this.deletedNoteData) {
|
||||
const region = this.resolveRegion(tracks, data.regionId);
|
||||
@@ -99,6 +134,18 @@ export class DeleteMidiEventsCommand extends KGCommand {
|
||||
core.removeSelectedItem(selectedPitchBend);
|
||||
}
|
||||
}
|
||||
|
||||
for (const data of this.deletedControllerEventData) {
|
||||
const region = this.resolveRegion(tracks, data.regionId);
|
||||
region.removeControllerEvent(data.controller, data.controllerEvent.getId());
|
||||
|
||||
const selectedControllerEvent = core.getSelectedItems().find(
|
||||
item => item instanceof KGMidiControllerEvent && item.getId() === data.controllerEvent.getId()
|
||||
);
|
||||
if (selectedControllerEvent) {
|
||||
core.removeSelectedItem(selectedControllerEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
@@ -125,19 +172,39 @@ export class DeleteMidiEventsCommand extends KGCommand {
|
||||
region.addPitchBend(data.pitchBend);
|
||||
}
|
||||
}
|
||||
|
||||
for (const data of [...this.deletedControllerEventData].sort((a, b) => a.originalIndex - b.originalIndex)) {
|
||||
const region = this.resolveRegion(tracks, data.regionId);
|
||||
const controllerEvents = region.getControllerEvents(data.controller);
|
||||
if (data.originalIndex >= 0 && data.originalIndex <= controllerEvents.length) {
|
||||
controllerEvents.splice(data.originalIndex, 0, data.controllerEvent);
|
||||
region.setControllerEvents(data.controller, controllerEvents);
|
||||
} else {
|
||||
region.addControllerEvent(data.controller, data.controllerEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteCount = this.noteIds.length;
|
||||
const pitchBendCount = this.pitchBendIds.length;
|
||||
|
||||
if (noteCount > 0 && pitchBendCount > 0) {
|
||||
return `Delete ${noteCount} note${noteCount === 1 ? '' : 's'} and ${pitchBendCount} pitch bend${pitchBendCount === 1 ? '' : 's'}`;
|
||||
const controllerEventCount = this.controllerEventIds.length;
|
||||
const parts: string[] = [];
|
||||
if (noteCount > 0) {
|
||||
parts.push(`${noteCount} note${noteCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
if (pitchBendCount > 0) {
|
||||
return pitchBendCount === 1 ? 'Delete pitch bend' : `Delete ${pitchBendCount} pitch bends`;
|
||||
parts.push(`${pitchBendCount} pitch bend${pitchBendCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
return noteCount === 1 ? 'Delete note' : `Delete ${noteCount} notes`;
|
||||
if (controllerEventCount > 0) {
|
||||
parts.push(`${controllerEventCount} controller event${controllerEventCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return 'Delete MIDI events';
|
||||
}
|
||||
|
||||
return `Delete ${parts.join(' and ')}`;
|
||||
}
|
||||
|
||||
private resolveRegion(tracks: Array<{ getRegions(): unknown[] }>, regionId: string): KGMidiRegion {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
|
||||
interface ControllerEventSnapshot {
|
||||
controllerEventId: string;
|
||||
controller: number;
|
||||
beat: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ControllerEventUpdate {
|
||||
controllerEventId: string;
|
||||
controller?: number;
|
||||
beat?: number;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export class UpdateControllerEventPropertiesCommand extends KGCommand {
|
||||
private regionId: string;
|
||||
private snapshots: ControllerEventSnapshot[];
|
||||
private updates: ControllerEventUpdate[];
|
||||
private targetRegion: KGMidiRegion | null = null;
|
||||
private parentTrack: KGTrack | null = null;
|
||||
|
||||
constructor(regionId: string, snapshots: ControllerEventSnapshot[], updates: ControllerEventUpdate[]) {
|
||||
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`);
|
||||
}
|
||||
|
||||
for (const update of this.updates) {
|
||||
const currentController = this.snapshots.find(snapshot => snapshot.controllerEventId === update.controllerEventId)?.controller;
|
||||
if (currentController === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = this.findControllerEvent(currentController, update.controllerEventId);
|
||||
if (!event) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (update.controller !== undefined && update.controller !== currentController) {
|
||||
this.targetRegion.removeControllerEvent(currentController, event.getId());
|
||||
this.targetRegion.addControllerEvent(update.controller, event);
|
||||
}
|
||||
|
||||
if (update.beat !== undefined) event.setBeat(update.beat);
|
||||
if (update.value !== undefined) event.setValue(update.value);
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetRegion) {
|
||||
throw new Error('Cannot undo: command was not executed');
|
||||
}
|
||||
|
||||
this.snapshots.forEach(snapshot => {
|
||||
const existing = this.findControllerEventAcrossBuckets(snapshot.controllerEventId);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.controller !== snapshot.controller) {
|
||||
this.targetRegion!.removeControllerEvent(existing.controller, existing.event.getId());
|
||||
this.targetRegion!.addControllerEvent(snapshot.controller, existing.event);
|
||||
}
|
||||
|
||||
existing.event.setBeat(snapshot.beat);
|
||||
existing.event.setValue(snapshot.value);
|
||||
});
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const count = this.snapshots.length;
|
||||
return count === 1 ? 'Update controller event properties' : `Update ${count} controller events' properties`;
|
||||
}
|
||||
|
||||
public getParentTrack(): KGTrack | null {
|
||||
return this.parentTrack;
|
||||
}
|
||||
|
||||
private findControllerEvent(controller: number, controllerEventId: string): KGMidiControllerEvent | undefined {
|
||||
return this.targetRegion?.getControllerEvents(controller).find(candidate => candidate.getId() === controllerEventId);
|
||||
}
|
||||
|
||||
private findControllerEventAcrossBuckets(controllerEventId: string): { controller: number; event: KGMidiControllerEvent } | null {
|
||||
if (!this.targetRegion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const { controller, event } of this.targetRegion.getAllControllerEventsFlattened()) {
|
||||
if (event.getId() === controllerEventId) {
|
||||
return { controller, event };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
@@ -22,6 +23,11 @@ interface RegionSnapshot {
|
||||
beat: number;
|
||||
value: number;
|
||||
}>;
|
||||
controllerEventsByType: Array<Array<{
|
||||
id: string;
|
||||
beat: number;
|
||||
value: number;
|
||||
}>>;
|
||||
}
|
||||
|
||||
interface ResolvedRegion {
|
||||
@@ -47,6 +53,14 @@ function clonePitchBend(pitchBend: KGMidiPitchBend, beat: number): KGMidiPitchBe
|
||||
);
|
||||
}
|
||||
|
||||
function cloneControllerEvent(controllerEvent: KGMidiControllerEvent, beat: number): KGMidiControllerEvent {
|
||||
return new KGMidiControllerEvent(
|
||||
controllerEvent.getId(),
|
||||
beat,
|
||||
controllerEvent.getValue()
|
||||
);
|
||||
}
|
||||
|
||||
export class MergeMidiRegionsCommand extends KGCommand {
|
||||
private readonly regionIdsToMerge: string[];
|
||||
private targetTrack: KGTrack | null = null;
|
||||
@@ -121,6 +135,13 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
beat: pitchBend.getBeat(),
|
||||
value: pitchBend.getValue(),
|
||||
})),
|
||||
controllerEventsByType: region.getControllerEventsByType().map(events => (
|
||||
events.map(event => ({
|
||||
id: event.getId(),
|
||||
beat: event.getBeat(),
|
||||
value: event.getValue(),
|
||||
}))
|
||||
)),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,6 +157,7 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
|
||||
const mergedNotes = [...this.survivingRegion.getNotes()];
|
||||
const mergedPitchBends = [...this.survivingRegion.getPitchBends()];
|
||||
const mergedControllerEventsByType = this.survivingRegion.getControllerEventsByType().map(events => [...events]);
|
||||
for (const { region } of resolvedRegions.slice(1)) {
|
||||
const regionStart = region.getStartFromBeat();
|
||||
region.getNotes().forEach(note => {
|
||||
@@ -153,11 +175,20 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
regionStart + pitchBend.getBeat() - survivingRegionStart
|
||||
));
|
||||
});
|
||||
region.getControllerEventsByType().forEach((events, controller) => {
|
||||
events.forEach(event => {
|
||||
mergedControllerEventsByType[controller].push(cloneControllerEvent(
|
||||
event,
|
||||
regionStart + event.getBeat() - survivingRegionStart
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.survivingRegion.setLength(mergedEndBeat - survivingRegionStart);
|
||||
this.survivingRegion.setNotes(mergedNotes);
|
||||
this.survivingRegion.setPitchBends(mergedPitchBends);
|
||||
this.survivingRegion.setControllerEventsByType(mergedControllerEventsByType);
|
||||
|
||||
const removedRegionIds = new Set(this.removedRegions.map(({ region }) => region.getId()));
|
||||
const nextRegions = resolvedTargetTrack.getRegions().filter(region => !removedRegionIds.has(region.getId()));
|
||||
@@ -197,6 +228,13 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
pitchBend.beat,
|
||||
pitchBend.value
|
||||
)));
|
||||
this.survivingRegion.setControllerEventsByType(survivingSnapshot.controllerEventsByType.map(events => (
|
||||
events.map(event => new KGMidiControllerEvent(
|
||||
event.id,
|
||||
event.beat,
|
||||
event.value
|
||||
))
|
||||
)));
|
||||
|
||||
for (const { region } of this.removedRegions) {
|
||||
const snapshot = this.originalRegionSnapshots.get(region.getId());
|
||||
@@ -217,6 +255,13 @@ export class MergeMidiRegionsCommand extends KGCommand {
|
||||
pitchBend.beat,
|
||||
pitchBend.value
|
||||
)));
|
||||
region.setControllerEventsByType(snapshot.controllerEventsByType.map(events => (
|
||||
events.map(event => new KGMidiControllerEvent(
|
||||
event.id,
|
||||
event.beat,
|
||||
event.value
|
||||
))
|
||||
)));
|
||||
}
|
||||
|
||||
const regions = [...this.targetTrack.getRegions()];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGRegion } from '../../region/KGRegion';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
@@ -90,6 +91,15 @@ export class PasteRegionsCommand extends KGCommand {
|
||||
pitchBend.getValue()
|
||||
));
|
||||
});
|
||||
originalRegion.getControllerEventsByType().forEach((events, controller) => {
|
||||
events.forEach(controllerEvent => {
|
||||
(newRegion as KGMidiRegion).addControllerEvent(controller, new KGMidiControllerEvent(
|
||||
generateUniqueId('KGMidiControllerEvent'),
|
||||
controllerEvent.getBeat(),
|
||||
controllerEvent.getValue()
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Created MIDI region "${newRegion.getName()}" with ${originalNotes.length} notes`);
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { KGCore } from '../../KGCore';
|
||||
import { KGRegion } from '../../region/KGRegion';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../region/KGAudioRegion';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
|
||||
@@ -28,6 +29,11 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
pitchBendId: string;
|
||||
originalBeat: number;
|
||||
}> = [];
|
||||
private controllerEventAdjustments: Array<{
|
||||
controller: number;
|
||||
controllerEventId: string;
|
||||
originalBeat: number;
|
||||
}> = [];
|
||||
|
||||
// Audio region clip offset support
|
||||
private newClipStartOffsetSeconds?: number;
|
||||
@@ -93,6 +99,16 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
});
|
||||
pitchBend.setBeat(pitchBend.getBeat() - beatOffset);
|
||||
});
|
||||
targetRegion.getControllerEventsByType().forEach((events, controller) => {
|
||||
events.forEach((controllerEvent: KGMidiControllerEvent) => {
|
||||
this.controllerEventAdjustments.push({
|
||||
controller,
|
||||
controllerEventId: controllerEvent.getId(),
|
||||
originalBeat: controllerEvent.getBeat(),
|
||||
});
|
||||
controllerEvent.setBeat(controllerEvent.getBeat() - beatOffset);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Adjusted ${notes.length} notes by offset ${-beatOffset} beats to maintain absolute positions`);
|
||||
}
|
||||
@@ -143,6 +159,16 @@ export class ResizeRegionCommand extends KGCommand {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.controllerEventAdjustments.length > 0 && this.targetRegion instanceof KGMidiRegion) {
|
||||
const midiRegion = this.targetRegion;
|
||||
this.controllerEventAdjustments.forEach(adjustment => {
|
||||
const controllerEvent = midiRegion.getControllerEvents(adjustment.controller)
|
||||
.find(candidate => candidate.getId() === adjustment.controllerEventId);
|
||||
if (controllerEvent) {
|
||||
controllerEvent.setBeat(adjustment.originalBeat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore clip offset for audio regions
|
||||
if (this.targetRegion instanceof KGAudioRegion && this.newClipStartOffsetSeconds !== undefined) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { KGCore } from '../../KGCore';
|
||||
import { KGRegion } from '../../region/KGRegion';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../region/KGAudioRegion';
|
||||
import { KGMidiControllerEvent } from '../../midi/KGMidiControllerEvent';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
@@ -131,6 +132,22 @@ export class SplitRegionCommand extends KGCommand {
|
||||
}
|
||||
}
|
||||
|
||||
for (const { controller, event } of originalRegion.getAllControllerEventsFlattened()) {
|
||||
if (event.getBeat() < splitOffsetBeats) {
|
||||
region1.addControllerEvent(controller, new KGMidiControllerEvent(
|
||||
generateUniqueId('KGMidiControllerEvent'),
|
||||
event.getBeat(),
|
||||
event.getValue()
|
||||
));
|
||||
} else {
|
||||
region2.addControllerEvent(controller, new KGMidiControllerEvent(
|
||||
generateUniqueId('KGMidiControllerEvent'),
|
||||
event.getBeat() - splitOffsetBeats,
|
||||
event.getValue()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
this.region1 = region1;
|
||||
this.region2 = region2;
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ interface PitchBendAdjustment {
|
||||
originalBeat: number;
|
||||
}
|
||||
|
||||
interface ControllerEventAdjustment {
|
||||
controller: number;
|
||||
controllerEventId: string;
|
||||
originalBeat: number;
|
||||
}
|
||||
|
||||
const EPSILON = 1e-9;
|
||||
|
||||
function getRegionById(tracks: KGTrack[], regionId: string): { region: KGRegion; track: KGTrack } | null {
|
||||
@@ -174,6 +180,7 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
private targetRegions: KGRegion[] = [];
|
||||
private noteAdjustments = new Map<string, NoteAdjustment[]>();
|
||||
private pitchBendAdjustments = new Map<string, PitchBendAdjustment[]>();
|
||||
private controllerEventAdjustments = new Map<string, ControllerEventAdjustment[]>();
|
||||
|
||||
constructor(
|
||||
primaryRegionId: string,
|
||||
@@ -272,6 +279,8 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
}));
|
||||
this.targetRegions = resolvedRegions.map(({ region }) => region);
|
||||
this.noteAdjustments.clear();
|
||||
this.pitchBendAdjustments.clear();
|
||||
this.controllerEventAdjustments.clear();
|
||||
|
||||
projectedStates.forEach(projectedState => {
|
||||
const region = projectedState.region;
|
||||
@@ -295,6 +304,16 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
region.getPitchBends().forEach(pitchBend => {
|
||||
pitchBend.setBeat(pitchBend.getBeat() - beatOffset);
|
||||
});
|
||||
this.controllerEventAdjustments.set(region.getId(), region.getAllControllerEventsFlattened().map(({ controller, event }) => ({
|
||||
controller,
|
||||
controllerEventId: event.getId(),
|
||||
originalBeat: event.getBeat(),
|
||||
})));
|
||||
region.getControllerEventsByType().forEach(events => {
|
||||
events.forEach(event => {
|
||||
event.setBeat(event.getBeat() - beatOffset);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (region instanceof KGAudioRegion && projectedState.clipStartOffsetSeconds !== undefined) {
|
||||
@@ -335,6 +354,14 @@ export class ResizeMultipleRegionsCommand extends KGCommand {
|
||||
pitchBend.setBeat(adjustment.originalBeat);
|
||||
}
|
||||
});
|
||||
const controllerEventAdjustments = this.controllerEventAdjustments.get(region.getId()) ?? [];
|
||||
controllerEventAdjustments.forEach(adjustment => {
|
||||
const controllerEvent = region.getControllerEvents(adjustment.controller)
|
||||
.find(candidate => candidate.getId() === adjustment.controllerEventId);
|
||||
if (controllerEvent) {
|
||||
controllerEvent.setBeat(adjustment.originalBeat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (region instanceof KGAudioRegion && originalState.clipStartOffsetSeconds !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user