Added comprehensive selection and editing capabilities to multi-track editor: - Visual selection overlay with Shift+drag interaction on waveforms - Multi-track edit commands (cut, copy, paste, delete, duplicate) - Full keyboard shortcut support (Ctrl+X/C/V/D, Delete, Ctrl+Z/Y) - Complete undo/redo integration via command pattern - Per-track selection state with localStorage persistence - Audio buffer manipulation utilities (extract, insert, delete, duplicate segments) - Toast notifications for all edit operations - Red playhead to distinguish from blue selection overlay All edit operations are fully undoable and integrated with the existing history manager system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
5.1 KiB
TypeScript
191 lines
5.1 KiB
TypeScript
/**
|
|
* Multi-track edit commands for audio operations across tracks
|
|
*/
|
|
|
|
import { BaseCommand } from '../command';
|
|
import type { Track } from '@/types/track';
|
|
import type { Selection } from '@/types/selection';
|
|
import {
|
|
extractBufferSegment,
|
|
deleteBufferSegment,
|
|
insertBufferSegment,
|
|
duplicateBufferSegment,
|
|
} from '@/lib/audio/buffer-utils';
|
|
|
|
export type MultiTrackEditType = 'cut' | 'copy' | 'delete' | 'paste' | 'duplicate';
|
|
|
|
export interface MultiTrackEditParams {
|
|
type: MultiTrackEditType;
|
|
trackId: string;
|
|
beforeBuffer: AudioBuffer | null;
|
|
afterBuffer: AudioBuffer | null;
|
|
selection?: Selection;
|
|
clipboardData?: AudioBuffer;
|
|
pastePosition?: number;
|
|
onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void;
|
|
}
|
|
|
|
/**
|
|
* Command for multi-track edit operations
|
|
*/
|
|
export class MultiTrackEditCommand extends BaseCommand {
|
|
private type: MultiTrackEditType;
|
|
private trackId: string;
|
|
private beforeBuffer: AudioBuffer | null;
|
|
private afterBuffer: AudioBuffer | null;
|
|
private selection?: Selection;
|
|
private clipboardData?: AudioBuffer;
|
|
private pastePosition?: number;
|
|
private onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void;
|
|
|
|
constructor(params: MultiTrackEditParams) {
|
|
super();
|
|
this.type = params.type;
|
|
this.trackId = params.trackId;
|
|
this.beforeBuffer = params.beforeBuffer;
|
|
this.afterBuffer = params.afterBuffer;
|
|
this.selection = params.selection;
|
|
this.clipboardData = params.clipboardData;
|
|
this.pastePosition = params.pastePosition;
|
|
this.onApply = params.onApply;
|
|
}
|
|
|
|
execute(): void {
|
|
// For copy, don't modify the buffer, just update selection
|
|
if (this.type === 'copy') {
|
|
this.onApply(this.trackId, this.beforeBuffer, this.selection || null);
|
|
} else {
|
|
this.onApply(this.trackId, this.afterBuffer, null);
|
|
}
|
|
}
|
|
|
|
undo(): void {
|
|
this.onApply(this.trackId, this.beforeBuffer, null);
|
|
}
|
|
|
|
getDescription(): string {
|
|
switch (this.type) {
|
|
case 'cut':
|
|
return 'Cut';
|
|
case 'copy':
|
|
return 'Copy';
|
|
case 'delete':
|
|
return 'Delete';
|
|
case 'paste':
|
|
return 'Paste';
|
|
case 'duplicate':
|
|
return 'Duplicate';
|
|
default:
|
|
return 'Edit';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Factory functions to create multi-track edit commands
|
|
*/
|
|
|
|
export function createMultiTrackCutCommand(
|
|
trackId: string,
|
|
buffer: AudioBuffer,
|
|
selection: Selection,
|
|
onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void
|
|
): MultiTrackEditCommand {
|
|
const afterBuffer = deleteBufferSegment(buffer, selection.start, selection.end);
|
|
|
|
return new MultiTrackEditCommand({
|
|
type: 'cut',
|
|
trackId,
|
|
beforeBuffer: buffer,
|
|
afterBuffer,
|
|
selection,
|
|
onApply,
|
|
});
|
|
}
|
|
|
|
export function createMultiTrackCopyCommand(
|
|
trackId: string,
|
|
buffer: AudioBuffer,
|
|
selection: Selection,
|
|
onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void
|
|
): MultiTrackEditCommand {
|
|
// Copy doesn't modify the buffer
|
|
return new MultiTrackEditCommand({
|
|
type: 'copy',
|
|
trackId,
|
|
beforeBuffer: buffer,
|
|
afterBuffer: buffer,
|
|
selection,
|
|
onApply,
|
|
});
|
|
}
|
|
|
|
export function createMultiTrackDeleteCommand(
|
|
trackId: string,
|
|
buffer: AudioBuffer,
|
|
selection: Selection,
|
|
onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void
|
|
): MultiTrackEditCommand {
|
|
const afterBuffer = deleteBufferSegment(buffer, selection.start, selection.end);
|
|
|
|
return new MultiTrackEditCommand({
|
|
type: 'delete',
|
|
trackId,
|
|
beforeBuffer: buffer,
|
|
afterBuffer,
|
|
selection,
|
|
onApply,
|
|
});
|
|
}
|
|
|
|
export function createMultiTrackPasteCommand(
|
|
trackId: string,
|
|
buffer: AudioBuffer | null,
|
|
clipboardData: AudioBuffer,
|
|
pastePosition: number,
|
|
onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void
|
|
): MultiTrackEditCommand {
|
|
const targetBuffer = buffer || createSilentBuffer(clipboardData.sampleRate, clipboardData.numberOfChannels, pastePosition);
|
|
const afterBuffer = insertBufferSegment(targetBuffer, clipboardData, pastePosition);
|
|
|
|
return new MultiTrackEditCommand({
|
|
type: 'paste',
|
|
trackId,
|
|
beforeBuffer: buffer,
|
|
afterBuffer,
|
|
clipboardData,
|
|
pastePosition,
|
|
onApply,
|
|
});
|
|
}
|
|
|
|
export function createMultiTrackDuplicateCommand(
|
|
trackId: string,
|
|
buffer: AudioBuffer,
|
|
selection: Selection,
|
|
onApply: (trackId: string, buffer: AudioBuffer | null, selection: Selection | null) => void
|
|
): MultiTrackEditCommand {
|
|
const afterBuffer = duplicateBufferSegment(buffer, selection.start, selection.end);
|
|
|
|
return new MultiTrackEditCommand({
|
|
type: 'duplicate',
|
|
trackId,
|
|
beforeBuffer: buffer,
|
|
afterBuffer,
|
|
selection,
|
|
onApply,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper function to create a silent buffer
|
|
*/
|
|
function createSilentBuffer(sampleRate: number, numberOfChannels: number, duration: number): AudioBuffer {
|
|
const audioContext = new OfflineAudioContext(
|
|
numberOfChannels,
|
|
Math.ceil(duration * sampleRate),
|
|
sampleRate
|
|
);
|
|
return audioContext.createBuffer(numberOfChannels, Math.ceil(duration * sampleRate), sampleRate);
|
|
}
|