@dekzer/wav-decoder
Version:
A robust, streaming-capable WAV audio decoder with full PCM, float, A-law, and µ-law support — zero dependencies, works in browsers and Node.js.
251 lines (185 loc) • 9.2 kB
Markdown
# @dekzer/wav-decoder
[](https://dekzer-oss.github.io/wav-decoder/)
[](https://github.com/dekzer-oss/wav-decoder/actions/workflows/deploy.yml)
[](https://nodejs.org/)
[](#supported-formats--limits)
[](#supported-formats--limits)
A TypeScript/JavaScript library for **progressive decoding** of uncompressed WAV audio files *as bytes
arrive*. Optimized for Chromium while maintaining compatibility with all modern browsers and Node.js 20+. Breaking
changes may occur before **v1.0.0**.
## Table of Contents
1. [Status & Goals](#status--goals)
2. [Features](#features)
3. [Installation](#installation)
4. [Quick Example](#quick-example)
5. [Live Demos](#live-demos)
6. [API](#api)
7. [Usage Examples](#usage-examples)
8. [Supported Formats & Limits](#supported-formats--limits)
9. [Development & Testing](#development--testing)
10. [License](#license)
## Status & Goals
| Aspect | Status |
|---------------|-------------------------------------------------------------------|
| **Maturity** | Internal prototype: functional but APIs not finalized |
| **Stability** | Pre-v1.0.0 releases may contain breaking API changes |
| **Roadmap** | Node.js throughput optimization, optional Worker/Worklet wrappers |
## Features
- **Progressive decoding** - Process audio chunks as they arrive
- **Zero dependencies** - Runtime dependency-free (dev-only in `package.json`)
- **Broad format support** - 8/16/24/32-bit PCM, 32/64-bit float, A-law/µ-law (little/big-endian)
- **Cross-platform** - Compatible with Node.js 20+ and modern browsers
- **AudioContext-ready** - Directly pipe `Float32Array[]` output to Web Audio API
- **Rigorous testing** - Validated against ~20 fixture WAV files
## Installation
```bash
# pnpm
pnpm add @dekzer/wav-decoder
# npm
npm install @dekzer/wav-decoder
```
No post-install scripts or optional binaries.
## Quick Example
```ts
import { WavDecoder } from '@dekzer/wav-decoder';
async function streamAndPlay(url: string) {
const decoder = new WavDecoder();
const response = await fetch(url);
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const out = decoder.decode(value);
if (out.samplesDecoded) {
playChunk(out.channelData, out.sampleRate);
}
}
const tail = decoder.flush();
if (tail.samplesDecoded) {
playChunk(tail.channelData, tail.sampleRate);
}
}
```
## Live Demos
Explore these interactive examples to accelerate integration:
| Demo | Description | Source |
|--------------------------------------------------------------------------------|-----------------------------------------------|------------------------------------------------|
| [Full UI Demo](https://dekzer-oss.github.io/wav-decoder) | Drag & drop, metrics, playback visualization | [`index.html`](index.html) |
| [Starter Demo](https://dekzer-oss.github.io/wav-decoder/starter) | Minimal implementation with progress tracking | [`starter.html`](starter.html) |
| [Streaming Playback](https://dekzer-oss.github.io/wav-decoder/stream-and-play) | Low-latency streaming decode & playback | [`stream-and-play.html`](stream-and-play.html) |
## API
### `class WavDecoder`
| Method | Description |
|-------------------------------|------------------------------------------------------------------|
| **constructor()** | Initializes with 64 KiB ring buffer (default) |
| **decode(chunk: Uint8Array)** | Processes incoming bytes; returns decoded samples |
| **flush()** | Finalizes decoding of remaining bytes (including partial blocks) |
| **reset()** | Clears state for instance reuse |
| **free()** | Releases resources and sets `info.state` to `ENDED` |
| **info** *(read-only)* | Real-time diagnostics object |
#### Output Structure (`DecodedWavAudio`)
```ts
{
channelData: Float32Array[]; // Per-channel audio buffers
samplesDecoded: number; // Samples decoded in this operation
sampleRate: number; // Extracted from WAV header
errors: DecodeError[]; // Non-fatal decoding issues
}
```
#### `decoder.info` Properties
| Property | Description |
|----------------|--------------------------------------------------------------|
| `state` | `DecoderState.IDLE` \| `DECODING` \| `ENDED` \| `ERROR` |
| `format` | Header details (`formatTag`, `channels`, `sampleRate`, etc.) |
| `decodedBytes` | Cumulative PCM bytes decoded |
| `progress` | Completion ratio (0–1, NaN if unknown) |
| `errors` | Recent non-fatal errors (fatal errors set `state = ERROR`) |
## Usage Examples
### Basic streaming (fetch)
```ts
import { WavDecoder } from '@dekzer/wav-decoder';
const decoder = new WavDecoder();
const response = await fetch(url);
for await (const chunk of response.body) {
const { channelData, samplesDecoded, sampleRate } = decoder.decode(chunk);
if (samplesDecoded) playChunk(channelData, sampleRate);
}
const tail = decoder.flush();
if (tail.samplesDecoded) playChunk(tail.channelData, tail.sampleRate);
```
### Local file processing (File/Blob)
```ts
async function processFile(file: File) {
const decoder = new WavDecoder();
for await (const chunk of file.stream()) {
const out = decoder.decode(chunk);
if (out.samplesDecoded) playChunk(out.channelData, out.sampleRate);
}
const tail = decoder.flush();
if (tail.samplesDecoded) playChunk(tail.channelData, tail.sampleRate);
}
```
### TransformStream integration
```ts
function createWavDecoder() {
const decoder = new WavDecoder();
return new TransformStream({
transform(chunk, controller) {
const out = decoder.decode(chunk);
if (out.samplesDecoded) controller.enqueue(out);
},
flush(controller) {
const tail = decoder.flush();
if (tail.samplesDecoded) controller.enqueue(tail);
}
});
}
await response.body
.pipeThrough(createWavDecoder())
.pipeTo(new WritableStream({
write({ channelData, sampleRate }) {
playChunk(channelData, sampleRate);
}
}));
```
## Supported Formats & Limits
| Category | Support |
|-----------------|----------------------------------------------------------------------------------|
| **Containers** | RIFF `WAVE` (LE), RIFX (BE) |
| **Encodings** | PCM, IEEE float, A-law, µ-law |
| **Bit Depth** | 8/16/24/32-bit int, 32/64-bit float |
| **Channels** | 1–8 (theoretically unlimited) |
| **Sample Rate** | ≤ 192 kHz |
| **Constraints** | Constant-memory streaming (no file size limits) |
| **Exclusions** | ADPCM, MPEG-WAV, broadcast extensions, cue lists |
| **Browsers** | Requires `ReadableStream` + `AudioContext` (Chrome 94+, Firefox 92+, Safari 15+) |
| **Node.js** | Version 20+ (simplified BYOB readers) |
### Browser Compatibility Notes
- **Async iteration over `ReadableStream`**: Chrome 124+, Firefox 110+, Edge 124+
- **Safari**: Use reader pattern for broader compatibility
- **All examples** provide fallback patterns for maximum browser support
## Development & Testing
```bash
pnpm install # Install dev dependencies
pnpm test # Run Vitest (Node + browser)
pnpm bench # Execute micro-benchmarks
pnpm demo # Launch Vite development server
```
- Continuous integration includes Vitest, Playwright tests, and benchmarks
- Fixtures generated via `scripts/gen-wav-fixtures.py` (no copyrighted material)
## License
MIT - See [LICENSE](./LICENSE)