A high-performance library for decoding audio files and timestamp ranges in browser environments. Powered by FFmpeg and compiled to WebAssembly via Emscripten.
- Standard Decoding: Decodes entire audio files or specific timestamp ranges synchronously or asynchronously (via Web Workers).
- Streaming Mode: Built to handle extremely large audio files (e.g., >24 hours) by dynamically requesting byte ranges over the network without buffering the entire file in memory.
- 2MB Range Cache: Minimizes network overhead in streaming mode by batch-fetching 2MB chunks and satisfying subsequent decodes from memory.
- PTS-Aligned Sample Accuracy: Leverages frame Presentation Timestamps (PTS) to guarantee sample-accurate waveform alignment, automatically padding gaps or discarding offsets.
- Multi-Channel Support: Supports returning interleaved multi-channel samples or auto-downmixing to mono.
- Dynamic URL Refreshing: Automatically handles expiring presigned URLs (e.g., AWS S3, GCS) by allowing hot-swapping of the stream URL in-flight without interrupting active decoders.
- Production Optimized: Statically built with Emscripten
-O3and-DNDEBUGoptimizations to suppress debug prints and reduce the worker bundle size by 30%.
- MP3 (via
libmp3lame) - WAV (PCM 8/16/24/32-bit, Float 32-bit)
- FLAC
- AAC / M4A
- OGG / Vorbis / Opus (via
libopus)
npm install @humansignal/audio-file-decoderLoads the entire file array buffer into WebAssembly memory. Useful for quick synchronous operations on smaller files.
import { getAudioDecoder } from '@humansignal/audio-file-decoder';
import decodeWasmUrl from '@humansignal/audio-file-decoder/decode-audio.wasm?url';
const fileOrArrayBuffer = await fetch('audio.mp3').then(r => r.arrayBuffer());
const decoder = await getAudioDecoder(decodeWasmUrl, fileOrArrayBuffer);
console.log({
sampleRate: decoder.sampleRate,
channelCount: decoder.channelCount,
encoding: decoder.encoding,
duration: decoder.duration // In seconds
});
// Decode the first 10 seconds (returns interleaved Float32Array)
const samples = decoder.decodeAudioData(0, 10, { multiChannel: true });
// Always dispose to free WASM heap resources
decoder.dispose();Runs the decoder inside a background Web Worker, keeping the main UI thread completely responsive.
import { getAudioDecoderWorker } from '@humansignal/audio-file-decoder';
import decodeWasmUrl from '@humansignal/audio-file-decoder/decode-audio.wasm?url';
const worker = await getAudioDecoderWorker(decodeWasmUrl, audioFileBlob, { stream: false });
// Decode a 30-second range asynchronously
const samples = await worker.decodeAudioData(60, 30, { multiChannel: false });
worker.dispose();Enables streaming chunk retrieval for large files. Instead of loading the full file, it uses HTTP Range requests in the worker.
import { getAudioDecoderWorker } from '@humansignal/audio-file-decoder';
import decodeWasmUrl from '@humansignal/audio-file-decoder/decode-audio.wasm?url';
// Passes a URL instead of a file/buffer to automatically enable streaming
const worker = await getAudioDecoderWorker(decodeWasmUrl, 'https://example.com/long-podcast.mp3', {
stream: true
});
// Decodes a slice. The worker only downloads the bytes needed for this range.
const samples = await worker.decodeAudioData(3600, 10);When loading files from S3 or proxy redirectors, signed URLs expire. The worker will propagate an HTTP_STATUS_403 or HTTP_STATUS_401 error which can be caught in the main thread to refresh the URL dynamically.
import { getAudioDecoderWorker } from '@humansignal/audio-file-decoder';
import decodeWasmUrl from '@humansignal/audio-file-decoder/decode-audio.wasm?url';
const originalUrl = '/api/media/redirect-to-s3';
const worker = await getAudioDecoderWorker(decodeWasmUrl, originalUrl, { stream: true });
async function safeDecode(start: number, duration: number) {
try {
return await worker.decodeAudioData(start, duration);
} catch (err: any) {
const isExpired = err?.message?.includes("HTTP_STATUS_403") || err?.message?.includes("HTTP_STATUS_401");
if (isExpired) {
console.warn("Presigned URL expired. Refreshing...");
// Fetch the redirect URL again to get a fresh presigned URL
const response = await fetch(originalUrl);
if (response.body) {
response.body.cancel().catch(() => {});
}
const freshPresignedUrl = response.url;
// Push the fresh URL to the worker in-place
worker.updateUrl(freshPresignedUrl);
// Retry the decode request
return await worker.decodeAudioData(start, duration);
}
throw err;
}
}Passed to decodeAudioData:
interface DecodeAudioOptions {
// Whether to preserve multiple channels.
// - If true: returns interleaved samples: [L0, R0, L1, R1, ...]
// - If false (default): downmixes to mono by averaging all channel samples
multiChannel?: boolean;
}Can be passed to getAudioDecoderWorker as the source parameter instead of a string to provide advanced details:
interface WasmAudioStreamConfig {
url?: string; // The stream URL
fileOrBlob?: Blob; // Local file handle (falls back to local FileReaderSync streaming)
size: number; // Total size of the file in bytes (avoids initial fetch)
}Import the WASM file with the ?url query suffix:
import decodeWasmUrl from '@humansignal/audio-file-decoder/decode-audio.wasm?url';Enable WebAssembly experiments in your webpack.config.js:
module.exports = {
experiments: {
asyncWebAssembly: true,
},
module: {
rules: [
{
test: /\.wasm$/,
type: 'asset/resource',
}
]
}
};- Docker (recommended for containerized building)
- Or, local Emscripten SDK (v2.0.1) and PKG_CONFIG dependencies.
To compile the C++ FFmpeg code and package the Javascript bundles:
# Build the WebAssembly binaries and JS bundles inside Docker (handles Emscripten)
./docker-build.sh quick
# Extract the compiled artifacts to the local ./dist folder
./docker-build.sh artifactsTo test edits instantly in a local consuming project (e.g. hs-platform) without manually copying files:
- Register this package globally:
# Inside /code/audio-file-decoder bun link - Link it in your consumer project:
# Inside the web app folder of your project bun link @humansignal/audio-file-decoder - Now, whenever you run
./docker-build.sh quick, the changes will immediately reflect in the consumer app.
By default, builds compile in optimized Release mode (-O3 -DNDEBUG), removing debug symbols and removing console log overhead.
If you need to debug the C++ core or Web Worker:
# Compile a debug build with -g and -O0
DEBUG=1 ./docker-build.sh quickLicensed under LGPL v2.1 or later. See the LICENSE file for details.