Fixed two issues: 1. Made audio editor full width by removing max-w-6xl wrapper 2. Fixed insertBufferSegment bug where paste operation had incorrect target buffer indexing causing corrupted audio after paste Changes: - app/page.tsx: Removed max-w-6xl constraint for full-width editor - lib/audio/buffer-utils.ts: Fixed paste buffer copying logic * Corrected target index calculation in "copy after insert point" loop * Was: targetData[insertBuffer.length + i] = sourceData[i] * Now: targetData[insertSample + insertBuffer.length + (i - insertSample)] = sourceData[i] 🤖 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[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;
|
|
}
|