Skip to content

Commit ade3564

Browse files
dyclaude
andcommitted
Add WebM/AAC/QOA/CAF encoders, opus/aiff/ogg metadata, wav 24-bit
New format encoders: - webm — Opus in a Matroska/EBML container, reuses opusscript (no new WASM) - aac — ADTS via native WebCodecs AudioEncoder (browser-only; throws in Node) - qoa — Quite OK Audio (pure JS, wraps qoa-format) - caf — Core Audio Format LPCM (16-bit int / 32-bit float) Metadata: - opus — VorbisComment baked into OpusTags at encode time (stays streaming) - aiff — ID3v2 chunk writer - ogg — VorbisComment rewrite, re-pages + re-CRCs, audio bit-identical - route `meta` per-format; correct markers/regions to wav-only Fixes: - wav — add 24-bit PCM; reject unsupported bitDepth (was silent corruption) - aiff/caf — reject unsupported bitDepth (same silent-corruption class) - remove empty packages/mp3-encode API: - encode.formats list + encode.mime map (named exports too) Versions: encode-audio 1.6.0; encode-wav 1.2.0; encode-opus/aiff/ogg 1.1.0; new encode-aac/caf/qoa/webm 1.0.0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0591429 commit ade3564

40 files changed

Lines changed: 2191 additions & 49 deletions

README.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ const buf = await encode.wav(channelData, { sampleRate: 44100 });
2020
| MP3 | [@audio/encode-mp3](https://npmjs.com/package/@audio/encode-mp3) | WASM |
2121
| OGG Vorbis | [@audio/encode-ogg](https://npmjs.com/package/@audio/encode-ogg) | WASM |
2222
| Opus | [@audio/encode-opus](https://npmjs.com/package/@audio/encode-opus) | WASM |
23+
| WebM | [@audio/encode-webm](https://npmjs.com/package/@audio/encode-webm) | WASM (Opus) |
2324
| FLAC | [@audio/encode-flac](https://npmjs.com/package/@audio/encode-flac) | WASM |
25+
| AAC | [@audio/encode-aac](https://npmjs.com/package/@audio/encode-aac) | WebCodecs* |
2426
| AIFF | [@audio/encode-aiff](https://npmjs.com/package/@audio/encode-aiff) | JS |
27+
| CAF | [@audio/encode-caf](https://npmjs.com/package/@audio/encode-caf) | JS |
28+
| QOA | [@audio/encode-qoa](https://npmjs.com/package/@audio/encode-qoa) | JS |
29+
30+
<sub>* AAC uses the native [WebCodecs](https://developer.mozilla.org/en-US/docs/Web/API/AudioEncoder) `AudioEncoder` — browser-only (Chromium/Safari), throws in Node.</sub>
2531

2632
### Whole-file encode
2733

@@ -32,12 +38,18 @@ import encode from 'encode-audio';
3238

3339
const wav = await encode.wav(channelData, { sampleRate: 44100 });
3440
const aiff = await encode.aiff(channelData, { sampleRate: 44100 });
41+
const caf = await encode.caf(channelData, { sampleRate: 44100 });
3542
const mp3 = await encode.mp3(channelData, { sampleRate: 44100, bitrate: 128 });
3643
const ogg = await encode.ogg(channelData, { sampleRate: 44100, quality: 5 });
3744
const flac = await encode.flac(channelData, { sampleRate: 44100 });
3845
const opus = await encode.opus(channelData, { sampleRate: 48000, bitrate: 96 });
46+
const webm = await encode.webm(channelData, { sampleRate: 48000, bitrate: 96 });
47+
const qoa = await encode.qoa(channelData, { sampleRate: 44100 });
48+
const aac = await encode.aac(channelData, { sampleRate: 44100, bitrate: 128 }); // browser only
3949
```
4050

51+
`encode.formats` lists the supported names and `encode.mime` maps each to a MIME type — handy for format-agnostic pipelines.
52+
4153
### Chunked encoding
4254

4355
Call with just options (no data) to create a streaming encoder:
@@ -73,23 +85,33 @@ Works with any async iterable source.
7385
| Option | Description | Applies to |
7486
|--------|-------------|------------|
7587
| `sampleRate` | Output sample rate (required) | all |
76-
| `bitrate` | Target bitrate in kbps | mp3, opus |
88+
| `bitrate` | Target bitrate in kbps | mp3, opus, webm, aac |
7789
| `quality` | Quality 0–10 (VBR) | ogg, mp3 |
7890
| `channels` | Output channel count | all |
79-
| `bitDepth` | Bit depth: 16 or 32 (wav), 16 or 24 (aiff, flac) | wav, aiff, flac |
91+
| `bitDepth` | Bit depth: 16/24/32 (wav), 16/24 (aiff, flac), 16/32 (caf) | wav, aiff, flac, caf |
8092
| `compression` | FLAC compression level 0–8 | flac |
81-
| `application` | `'audio'`, `'voip'`, or `'lowdelay'` | opus |
93+
| `application` | `'audio'`, `'voip'`, or `'lowdelay'` | opus, webm |
94+
| `meta` | Tags (see below) | wav, mp3, flac, aiff, ogg, opus |
8295

8396

8497
### Metadata
8598

86-
Splice tags, pictures, markers and regions into encoded bytes. Available for `wav`, `mp3`, `flac`.
99+
Pass `meta` (and, for `wav`, `markers`/`regions`) straight to the encoder:
100+
101+
```js
102+
let bytes = await encode.flac(channelData, {
103+
sampleRate: 44100,
104+
meta: { title: 'Hare Krishna', artist: 'Prabhupada', year: '1966' }
105+
})
106+
```
107+
108+
Tags work for `wav`, `mp3`, `flac`, `aiff`, `ogg` and `opus`. Cue `markers` and `regions` are `wav`-only. `opus` bakes tags into the OpusTags header at encode time (stays fully streaming); the others splice tags into the finished file — so passing `meta` to a **streaming/chunked** encode of `wav`/`mp3`/`flac`/`aiff`/`ogg` buffers the output and emits it on flush.
109+
110+
You can also tag already-encoded bytes via `encode-audio/meta`:
87111

88112
```js
89-
import encode from 'encode-audio'
90113
import { wav } from 'encode-audio/meta'
91114

92-
let bytes = await encode.wav(channelData, { sampleRate: 44100 })
93115
let out = wav(bytes, {
94116
meta: { title: 'Hare Krishna', artist: 'Prabhupada', year: '1966' },
95117
markers: [{ sample: 44100, label: 'verse' }],

audio-encode.d.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
type AudioInput = Float32Array[] | Float32Array | { numberOfChannels: number; getChannelData(i: number): Float32Array };
22

3+
export interface Meta {
4+
title?: string; artist?: string; album?: string; albumartist?: string;
5+
composer?: string; genre?: string; year?: string | number; track?: string | number;
6+
disc?: string | number; bpm?: string | number; key?: string; comment?: string;
7+
copyright?: string; isrc?: string; publisher?: string; software?: string; lyrics?: string;
8+
pictures?: { mime?: string; description?: string; type?: number; data: Uint8Array }[];
9+
[key: string]: any;
10+
}
11+
12+
export interface Marker { sample: number; label?: string; }
13+
export interface Region { sample: number; length: number; label?: string; }
14+
315
export interface EncodeOptions {
416
/** Output sample rate (required). */
517
sampleRate: number;
618
/** Output channel count. */
719
channels?: number;
8-
/** Target bitrate in kbps (lossy). */
20+
/** Target bitrate in kbps (lossy: mp3, opus, webm, aac). */
921
bitrate?: number;
10-
/** Quality 0-10 (VBR, format-specific). */
22+
/** Quality 0-10 (VBR, format-specific: ogg, mp3). */
1123
quality?: number;
12-
/** Bit depth: 16|32 for wav, 16|24 for aiff/flac. */
24+
/** Bit depth: 16|24|32 for wav, 16|24 for aiff/flac, 16|32 for caf. */
1325
bitDepth?: number;
1426
/** FLAC compression level 0-8. */
1527
compression?: number;
16-
/** Opus application: 'audio', 'voip', 'lowdelay'. */
28+
/** Opus/WebM application: 'audio', 'voip', 'lowdelay'. */
1729
application?: string;
30+
/** Tags. Baked in for opus; for wav/mp3/flac/aiff/ogg also available via encode-audio/meta. */
31+
meta?: Meta;
32+
/** Cue markers (wav). */
33+
markers?: Marker[];
34+
/** Labeled regions (wav). */
35+
regions?: Region[];
1836
[key: string]: any;
1937
}
2038

@@ -48,15 +66,31 @@ declare const encode: {
4866

4967
wav: FormatEncoder;
5068
aiff: FormatEncoder;
69+
caf: FormatEncoder;
5170
mp3: FormatEncoder;
5271
ogg: FormatEncoder;
5372
flac: FormatEncoder;
5473
opus: FormatEncoder;
55-
[format: string]: FormatEncoder;
74+
/** WebM (Opus). */
75+
webm: FormatEncoder;
76+
/** AAC (ADTS) — browser-only via WebCodecs; throws in Node. */
77+
aac: FormatEncoder;
78+
/** QOA (Quite OK Audio). */
79+
qoa: FormatEncoder;
80+
/** Supported format names. */
81+
formats: string[];
82+
/** Format → MIME type. */
83+
mime: Record<string, string>;
84+
[format: string]: any;
5685
};
5786

5887
export default encode;
5988

89+
/** Supported format names. */
90+
export const formats: string[];
91+
/** Format → MIME type map. */
92+
export const mime: Record<string, string>;
93+
6094
/** Chunked encode from async iterable. */
6195
export function encodeChunked(
6296
source: AsyncIterable<AudioInput>,

audio-encode.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,22 @@ const META_WRITERS = {
5757
wav: () => import('@audio/encode-wav/meta').then(m => m.writeMeta),
5858
mp3: () => import('@audio/encode-mp3/meta').then(m => m.writeMeta),
5959
flac: () => import('@audio/encode-flac/meta').then(m => m.writeMeta),
60+
aiff: () => import('@audio/encode-aiff/meta').then(m => m.writeMeta),
61+
ogg: () => import('@audio/encode-ogg/meta').then(m => m.writeMeta),
6062
}
6163

64+
// Formats that bake metadata into the encoder itself (Ogg OpusTags) — streamed,
65+
// never buffered. The rest splice meta post-hoc via META_WRITERS on flush.
66+
const NATIVE_META = new Set(['opus'])
67+
6268
function reg(name, load) {
6369
encode[name] = fmt(name, async (opts) => {
6470
let { meta, markers, regions, ...rest } = opts || {}
71+
let hasMeta = meta || markers?.length || regions?.length
6572
let init = (await load()).default
66-
let codec = await init(rest)
67-
// No meta requested (or unsupported by format): pass through.
68-
if (!META_WRITERS[name] || !(meta || markers?.length || regions?.length))
73+
let codec = await init(NATIVE_META.has(name) && hasMeta ? { ...rest, meta } : rest)
74+
// Native-meta, no post-splice writer, or no meta requested: pass through.
75+
if (NATIVE_META.has(name) || !META_WRITERS[name] || !hasMeta)
6976
return streamEncoder(ch => codec.encode(ch), () => codec.flush(), () => codec.free())
7077
// Meta requested: buffer encoder output, splice via writeMeta on flush.
7178
let writeMeta = await META_WRITERS[name]()
@@ -86,10 +93,24 @@ function reg(name, load) {
8693

8794
reg('wav', () => import('@audio/encode-wav'))
8895
reg('aiff', () => import('@audio/encode-aiff'))
96+
reg('caf', () => import('@audio/encode-caf'))
8997
reg('mp3', () => import('@audio/encode-mp3'))
9098
reg('ogg', () => import('@audio/encode-ogg'))
9199
reg('flac', () => import('@audio/encode-flac'))
92100
reg('opus', () => import('@audio/encode-opus'))
101+
reg('webm', () => import('@audio/encode-webm'))
102+
reg('aac', () => import('@audio/encode-aac'))
103+
reg('qoa', () => import('@audio/encode-qoa'))
104+
105+
// Supported format names and their MIME types — for format-agnostic pipelines.
106+
export const formats = ['wav', 'aiff', 'caf', 'mp3', 'ogg', 'flac', 'opus', 'webm', 'aac', 'qoa']
107+
export const mime = {
108+
wav: 'audio/wav', aiff: 'audio/aiff', caf: 'audio/x-caf',
109+
mp3: 'audio/mpeg', ogg: 'audio/ogg', flac: 'audio/flac',
110+
opus: 'audio/ogg', webm: 'audio/webm', aac: 'audio/aac', qoa: 'audio/qoa',
111+
}
112+
encode.formats = formats
113+
encode.mime = mime
93114

94115
/**
95116
* Wrap a stream factory into whole-file encoder + streaming

meta.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
* Meta writers — re-export from codec packages.
33
* @module encode-audio/meta
44
*
5-
* import { wav, mp3, flac } from 'encode-audio/meta'
5+
* import { wav, mp3, flac, aiff, ogg } from 'encode-audio/meta'
66
* let out = wav(bytes, { meta, markers, regions })
7+
*
8+
* Opus bakes tags at encode time — pass `meta` to `encode.opus(...)` directly.
79
*/
810

911
export { writeMeta as wav } from '@audio/encode-wav/meta'
1012
export { writeMeta as mp3 } from '@audio/encode-mp3/meta'
1113
export { writeMeta as flac } from '@audio/encode-flac/meta'
14+
export { writeMeta as aiff } from '@audio/encode-aiff/meta'
15+
export { writeMeta as ogg } from '@audio/encode-ogg/meta'

package-lock.json

Lines changed: 68 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)