Browse Source

fix(quick-003): use Web Audio API instead of MediaBunny for audio decoding

MediaBunny's Input/BlobSource is designed for video containers and cannot
parse standalone audio files (MP3, WAV, etc.), causing "unsupported format"
errors at getAudioTracks(). Replace with Web Audio API's decodeAudioData
which natively handles all browser-supported audio formats.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
13fb99d62b
  1. 104
      src/hooks/useAudioMixing.ts

104
src/hooks/useAudioMixing.ts

@ -1,12 +1,6 @@
'use client'; 'use client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import {
Input,
AudioBufferSink,
BlobSource,
ALL_FORMATS,
} from 'mediabunny';
const MINIMUM_AUDIO_DURATION = 0.1; const MINIMUM_AUDIO_DURATION = 0.1;
@ -36,7 +30,8 @@ interface UseAudioMixingReturn {
} }
/** /**
* Standalone async function to prepare audio for mixing with video * Standalone async function to prepare audio for mixing with video.
* Uses Web Audio API to decode standalone audio files (MP3, WAV, OGG, etc.).
*/ */
export async function prepareAudioAsync( export async function prepareAudioAsync(
audioBlob: Blob, audioBlob: Blob,
@ -46,36 +41,24 @@ export async function prepareAudioAsync(
): Promise<AudioData | null> { ): Promise<AudioData | null> {
try { try {
onProgress?.({ message: 'Loading audio file...', progress: 10 }); onProgress?.({ message: 'Loading audio file...', progress: 10 });
const blobSource = new BlobSource(audioBlob); const arrayBuffer = await audioBlob.arrayBuffer();
const input = new Input({ source: blobSource, formats: ALL_FORMATS });
onProgress?.({ message: 'Reading audio tracks...', progress: 20 });
const audioTracks = await input.getAudioTracks();
if (audioTracks.length === 0) {
throw new Error('No audio tracks found in file');
}
const audioTrack = audioTracks[0];
const sink = new AudioBufferSink(audioTrack);
const audioDuration = await input.computeDuration();
onProgress?.({ message: 'Decoding audio...', progress: 30 }); onProgress?.({ message: 'Decoding audio...', progress: 30 });
const decodedBuffers: AudioBuffer[] = []; const audioContext = new AudioContext();
for await (const wrappedBuffer of sink.buffers(0, Math.max(videoDuration, audioDuration))) { let decoded: AudioBuffer;
if (wrappedBuffer?.buffer) { try {
decodedBuffers.push(wrappedBuffer.buffer); decoded = await audioContext.decodeAudioData(arrayBuffer);
} } finally {
await audioContext.close();
} }
if (decodedBuffers.length === 0) { const sampleRate = decoded.sampleRate;
throw new Error('Failed to decode audio'); const channels = decoded.numberOfChannels;
}
const sampleRate = decodedBuffers[0].sampleRate;
const channels = decodedBuffers[0].numberOfChannels;
const targetDuration = Math.max(MINIMUM_AUDIO_DURATION, videoDuration); const targetDuration = Math.max(MINIMUM_AUDIO_DURATION, videoDuration);
const totalSamples = Math.max(1, Math.floor(targetDuration * sampleRate)); const totalSamples = Math.max(1, Math.floor(targetDuration * sampleRate));
onProgress?.({ message: 'Processing audio...', progress: 60 });
const mergedBuffer = new AudioBuffer({ const mergedBuffer = new AudioBuffer({
length: totalSamples, length: totalSamples,
numberOfChannels: channels, numberOfChannels: channels,
@ -86,62 +69,33 @@ export async function prepareAudioAsync(
const offsetSamples = Math.floor(offsetSeconds * sampleRate); const offsetSamples = Math.floor(offsetSeconds * sampleRate);
let writeOffset = 0; let writeOffset = 0;
let sourceSkipSamples = 0; let sourceOffset = 0;
if (offsetSamples > 0) { if (offsetSamples > 0) {
writeOffset = Math.min(offsetSamples, totalSamples); writeOffset = Math.min(offsetSamples, totalSamples);
} else if (offsetSamples < 0) { } else if (offsetSamples < 0) {
sourceSkipSamples = Math.abs(offsetSamples); sourceOffset = Math.abs(offsetSamples);
} }
let samplesToSkip = sourceSkipSamples; // Copy decoded audio into the target buffer
let bufferIndex = 0; const copyLength = Math.min(decoded.length - sourceOffset, totalSamples - writeOffset);
let bufferOffset = 0; if (copyLength > 0) {
while (samplesToSkip > 0 && bufferIndex < decodedBuffers.length) {
const buffer = decodedBuffers[bufferIndex];
const availableInBuffer = buffer.length - bufferOffset;
if (samplesToSkip >= availableInBuffer) {
samplesToSkip -= availableInBuffer;
bufferIndex++;
bufferOffset = 0;
} else {
bufferOffset = samplesToSkip;
samplesToSkip = 0;
}
}
while (writeOffset < totalSamples && bufferIndex < decodedBuffers.length) {
const buffer = decodedBuffers[bufferIndex];
const remainingSamples = totalSamples - writeOffset;
const availableInBuffer = buffer.length - bufferOffset;
const writeLength = Math.min(availableInBuffer, remainingSamples);
for (let channel = 0; channel < channels; channel++) { for (let channel = 0; channel < channels; channel++) {
const channelData = buffer.getChannelData(channel).subarray(bufferOffset, bufferOffset + writeLength); const sourceData = decoded.getChannelData(channel).subarray(sourceOffset, sourceOffset + copyLength);
mergedBuffer.getChannelData(channel).set(channelData, writeOffset); mergedBuffer.getChannelData(channel).set(sourceData, writeOffset);
}
writeOffset += writeLength;
bufferOffset += writeLength;
if (bufferOffset >= buffer.length) {
bufferIndex++;
bufferOffset = 0;
} }
writeOffset += copyLength;
} }
while (writeOffset < totalSamples && decodedBuffers.length > 0) { // Loop audio to fill remaining duration if needed
for (const buffer of decodedBuffers) { while (writeOffset < totalSamples) {
const remainingSamples = totalSamples - writeOffset; const remaining = totalSamples - writeOffset;
if (remainingSamples <= 0) break; const loopLength = Math.min(decoded.length, remaining);
const writeLength = Math.min(buffer.length, remainingSamples); for (let channel = 0; channel < channels; channel++) {
for (let channel = 0; channel < channels; channel++) { const sourceData = decoded.getChannelData(channel).subarray(0, loopLength);
const channelData = buffer.getChannelData(channel).subarray(0, writeLength); mergedBuffer.getChannelData(channel).set(sourceData, writeOffset);
mergedBuffer.getChannelData(channel).set(channelData, writeOffset);
}
writeOffset += writeLength;
} }
writeOffset += loopLength;
} }
applyFades(mergedBuffer, options); applyFades(mergedBuffer, options);

Loading…
Cancel
Save