Files
audio-ui/lib/history/commands/multi-track-edit-command.ts
Sebastian Krüger 74879a42cf feat: implement multi-track waveform selection and editing with undo/redo
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>
2025-11-18 13:05:05 +01:00

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);
}