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>
180 lines
4.5 KiB
TypeScript
180 lines
4.5 KiB
TypeScript
/**
|
|
* AudioBuffer manipulation utilities
|
|
*/
|
|
|
|
import { getAudioContext } from './context';
|
|
|
|
/**
|
|
* Extract a portion of an AudioBuffer
|
|
*/
|
|
export function extractBufferSegment(
|
|
buffer: AudioBuffer,
|
|
startTime: number,
|
|
endTime: number
|
|
): AudioBuffer {
|
|
const audioContext = getAudioContext();
|
|
const startSample = Math.floor(startTime * buffer.sampleRate);
|
|
const endSample = Math.floor(endTime * buffer.sampleRate);
|
|
const length = endSample - startSample;
|
|
|
|
const segment = audioContext.createBuffer(
|
|
buffer.numberOfChannels,
|
|
length,
|
|
buffer.sampleRate
|
|
);
|
|
|
|
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
|
const sourceData = buffer.getChannelData(channel);
|
|
const targetData = segment.getChannelData(channel);
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
targetData[i] = sourceData[startSample + i];
|
|
}
|
|
}
|
|
|
|
return segment;
|
|
}
|
|
|
|
/**
|
|
* Delete a portion of an AudioBuffer
|
|
*/
|
|
export function deleteBufferSegment(
|
|
buffer: AudioBuffer,
|
|
startTime: number,
|
|
endTime: number
|
|
): AudioBuffer {
|
|
const audioContext = getAudioContext();
|
|
const startSample = Math.floor(startTime * buffer.sampleRate);
|
|
const endSample = Math.floor(endTime * buffer.sampleRate);
|
|
|
|
const beforeLength = startSample;
|
|
const afterLength = buffer.length - endSample;
|
|
const newLength = beforeLength + afterLength;
|
|
|
|
const newBuffer = audioContext.createBuffer(
|
|
buffer.numberOfChannels,
|
|
newLength,
|
|
buffer.sampleRate
|
|
);
|
|
|
|
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
|
const sourceData = buffer.getChannelData(channel);
|
|
const targetData = newBuffer.getChannelData(channel);
|
|
|
|
// Copy before segment
|
|
for (let i = 0; i < beforeLength; i++) {
|
|
targetData[i] = sourceData[i];
|
|
}
|
|
|
|
// Copy after segment
|
|
for (let i = 0; i < afterLength; i++) {
|
|
targetData[beforeLength + i] = sourceData[endSample + i];
|
|
}
|
|
}
|
|
|
|
return newBuffer;
|
|
}
|
|
|
|
/**
|
|
* Insert an AudioBuffer at a specific position
|
|
*/
|
|
export function insertBufferSegment(
|
|
buffer: AudioBuffer,
|
|
insertBuffer: AudioBuffer,
|
|
insertTime: number
|
|
): AudioBuffer {
|
|
const audioContext = getAudioContext();
|
|
const insertSample = Math.floor(insertTime * buffer.sampleRate);
|
|
const newLength = buffer.length + insertBuffer.length;
|
|
|
|
const newBuffer = audioContext.createBuffer(
|
|
buffer.numberOfChannels,
|
|
newLength,
|
|
buffer.sampleRate
|
|
);
|
|
|
|
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
|
const sourceData = buffer.getChannelData(channel);
|
|
const insertData = insertBuffer.getChannelData(
|
|
Math.min(channel, insertBuffer.numberOfChannels - 1)
|
|
);
|
|
const targetData = newBuffer.getChannelData(channel);
|
|
|
|
// Copy before insert point
|
|
for (let i = 0; i < insertSample; i++) {
|
|
targetData[i] = sourceData[i];
|
|
}
|
|
|
|
// Copy insert buffer
|
|
for (let i = 0; i < insertBuffer.length; i++) {
|
|
targetData[insertSample + i] = insertData[i];
|
|
}
|
|
|
|
// Copy after insert point
|
|
for (let i = insertSample; i < buffer.length; i++) {
|
|
targetData[insertSample + insertBuffer.length + (i - insertSample)] = sourceData[i];
|
|
}
|
|
}
|
|
|
|
return newBuffer;
|
|
}
|
|
|
|
/**
|
|
* Trim buffer to selection
|
|
*/
|
|
export function trimBuffer(
|
|
buffer: AudioBuffer,
|
|
startTime: number,
|
|
endTime: number
|
|
): AudioBuffer {
|
|
return extractBufferSegment(buffer, startTime, endTime);
|
|
}
|
|
|
|
/**
|
|
* Concatenate two audio buffers
|
|
*/
|
|
export function concatenateBuffers(
|
|
buffer1: AudioBuffer,
|
|
buffer2: AudioBuffer
|
|
): AudioBuffer {
|
|
const audioContext = getAudioContext();
|
|
const newLength = buffer1.length + buffer2.length;
|
|
const channels = Math.max(buffer1.numberOfChannels, buffer2.numberOfChannels);
|
|
|
|
const newBuffer = audioContext.createBuffer(
|
|
channels,
|
|
newLength,
|
|
buffer1.sampleRate
|
|
);
|
|
|
|
for (let channel = 0; channel < channels; channel++) {
|
|
const targetData = newBuffer.getChannelData(channel);
|
|
|
|
// Copy first buffer
|
|
if (channel < buffer1.numberOfChannels) {
|
|
const data1 = buffer1.getChannelData(channel);
|
|
targetData.set(data1, 0);
|
|
}
|
|
|
|
// Copy second buffer
|
|
if (channel < buffer2.numberOfChannels) {
|
|
const data2 = buffer2.getChannelData(channel);
|
|
targetData.set(data2, buffer1.length);
|
|
}
|
|
}
|
|
|
|
return newBuffer;
|
|
}
|
|
|
|
/**
|
|
* Duplicate a segment of audio buffer (extract and insert it after the selection)
|
|
*/
|
|
export function duplicateBufferSegment(
|
|
buffer: AudioBuffer,
|
|
startTime: number,
|
|
endTime: number
|
|
): AudioBuffer {
|
|
const segment = extractBufferSegment(buffer, startTime, endTime);
|
|
return insertBufferSegment(buffer, segment, endTime);
|
|
}
|