Added comprehensive audio editing capabilities: - Region selection with Shift+drag on waveform - Visual selection feedback with blue overlay and borders - AudioBuffer manipulation utilities (cut, copy, paste, delete, trim) - EditControls UI component with edit buttons - Keyboard shortcuts (Ctrl+A, Ctrl+X, Ctrl+C, Ctrl+V, Delete, Escape) - Clipboard management for cut/copy/paste operations - Updated useAudioPlayer hook with loadBuffer method New files: - types/selection.ts - Selection and ClipboardData interfaces - lib/audio/buffer-utils.ts - AudioBuffer manipulation utilities - components/editor/EditControls.tsx - Edit controls UI Modified files: - components/editor/Waveform.tsx - Added selection support - components/editor/AudioEditor.tsx - Integrated edit operations - lib/hooks/useAudioPlayer.ts - Added loadBuffer method 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
168 lines
4.2 KiB
TypeScript
168 lines
4.2 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[insertBuffer.length + i] = 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;
|
|
}
|