Try it in the browser: Audio converter, Audio recorder. Runs on this package, nothing is uploaded.
Encode raw audio samples to any format.
JS / WASM – no ffmpeg, no native bindings, works in both node and browser.
Small API, minimal size, near-native performance, stream encoding.
import encode from '@audio/encode';
const buf = await encode.wav(channelData, { sampleRate: 44100 });| Format | Package | Engine |
|---|---|---|
| WAV | @audio/encode-wav | JS |
| MP3 | @audio/encode-mp3 | WASM |
| OGG Vorbis | @audio/encode-ogg | WASM |
| Opus | @audio/encode-opus | WASM (libopus, single file) |
| WebM | @audio/encode-webm | WASM (libopus, single file) |
| FLAC | @audio/encode-flac | WASM |
| AAC | @audio/encode-aac | WebCodecs*, else WASM (Fraunhofer FDK): LC, HE, HEv2 |
| AIFF | @audio/encode-aiff | JS |
| CAF | @audio/encode-caf | JS |
| QOA | @audio/encode-qoa | JS |
WavPack (wv) |
@audio/encode-wavpack | WASM (libwavpack, single file) — lossless, hybrid lossy, 16/24/float, APEv2 tags |
M4A / MP4 (m4a, mp4) |
@audio/encode-mp4 | JS muxer — AAC, Opus, FLAC, MP3 or PCM in ISOBMFF, iTunes tags + chapters, fragmented when streamed; remux() replaces or strips the audio track of an existing video without touching the video stream |
ALAC (m4a with codec: 'alac') |
@audio/encode-alac | JS — Apple Lossless, port of Apple's reference encoder (Apache-2.0), bit-exact |
* AAC uses the native WebCodecs AudioEncoder where the browser supports the configuration (Chromium, Safari), else the Fraunhofer FDK AAC encoder compiled to WebAssembly (Node, Firefox), loaded on first use.
Specify the format as method name. Input is Float32Array[] (one per channel), a single Float32Array (mono), or an AudioBuffer.
import encode from '@audio/encode';
const wav = await encode.wav(channelData, { sampleRate: 44100 });
const aiff = await encode.aiff(channelData, { sampleRate: 44100 });
const caf = await encode.caf(channelData, { sampleRate: 44100 });
const mp3 = await encode.mp3(channelData, { sampleRate: 44100, bitrate: 128 });
const ogg = await encode.ogg(channelData, { sampleRate: 44100, quality: 5 });
const flac = await encode.flac(channelData, { sampleRate: 44100 });
const opus = await encode.opus(channelData, { sampleRate: 48000, bitrate: 96 });
const webm = await encode.webm(channelData, { sampleRate: 48000, bitrate: 96 });
const qoa = await encode.qoa(channelData, { sampleRate: 44100 });
const aac = await encode.aac(channelData, { sampleRate: 44100, bitrate: 128 });encode.formats lists the supported names and encode.mime maps each to a MIME type — handy for format-agnostic pipelines.
Call with just options (no data) to create a streaming encoder:
import encode from '@audio/encode';
const enc = await encode.mp3({ sampleRate: 44100, bitrate: 128 });
const a = await enc(chunk1); // Uint8Array
const b = await enc(chunk2);
const c = await enc(null); // end of stream — flush + free
// explicit control: enc.flush(), enc.free()Pass an async iterable as data — returns an async generator:
import encode from '@audio/encode'
for await (let buf of encode.mp3(audioSource, { sampleRate: 44100, bitrate: 128 })) {
// buf is Uint8Array
}Works with any async iterable source.
stream: true emits bytes as they encode, metadata in the header, memory flat however long it runs (a pipe, a socket, a week of recording). Totals the header can't know yet say "unknown": WAV/AIFF sizes 0xFFFFFFFF (read to the end), CAF's -1 data size, QOA's samples: 0, FLAC's STREAMINFO zeros; m4a/mp4 become fragmented (moof+mdat per second). After the end, enc.head() returns the header with its final totals — write it over the start of a file; a pipe keeps the "unknown" one.
import { open } from 'node:fs/promises'
const file = await open('live.wav', 'w')
const enc = await encode.wav({ sampleRate: 48000, channels: 2, stream: true, meta: { title: 'Live' } })
for await (let pcm of source) await file.write(await enc(pcm))
await file.write(await enc())
let head = enc.head() // RF64 past 4 GB
if (head) await file.write(head, 0, head.length, 0)
await file.close()| Option | Description | Applies to |
|---|---|---|
sampleRate |
Output sample rate (required) | all |
bitrate |
Target bitrate in kbps | mp3, opus, webm, aac |
quality |
Quality 0–10 (VBR) | ogg, mp3 |
channels |
Output channel count | all |
bitDepth |
Bit depth: 16/24/32 (wav), 16/24 (aiff, flac), 16/32 (caf) | wav, aiff, flac, caf |
compression |
FLAC compression level 0–8 | flac |
application |
'audio', 'voip', or 'lowdelay' |
opus, webm |
meta |
Tags (see below) | wav, mp3, flac, aiff, ogg, opus, m4a |
chapters |
[{ time, title }] (seconds): ID3 CHAP/CTOC, iTunes chpl |
mp3, m4a |
profile |
'lc', 'he' (HE-AAC), 'hev2' (HE-AACv2, stereo) |
aac, m4a |
stream |
Emit as it encodes; head() after (see above) |
all |
frames |
With stream: the exact length, when known upfront, so the header goes out exact (a canonical WAV; a pipe gets true totals) |
wav, aiff, caf, qoa, flac, mp3 |
Pass meta (and, for wav, markers/regions) straight to the encoder:
let bytes = await encode.flac(channelData, {
sampleRate: 44100,
meta: { title: 'Hare Krishna', artist: 'Prabhupada', year: '1966' }
})Tags work for wav, mp3, flac, aiff, ogg, opus and m4a. Cue markers and regions are wav-only. opus, wv and m4a bake tags into their headers as they encode; the others splice tags into the finished file, so a chunked encode of wav/mp3/flac/aiff/ogg with meta buffers until flush — unless stream: true, which writes the tags ahead of the audio and streams.
You can also tag already-encoded bytes via @audio/encode/meta:
import { wav } from '@audio/encode/meta'
let out = wav(bytes, {
meta: { title: 'Hare Krishna', artist: 'Prabhupada', year: '1966' },
markers: [{ sample: 44100, label: 'verse' }],
regions: [{ sample: 88200, length: 44100, label: 'chorus' }]
})Each codec sub-package also exposes its writer directly:
import { writeMeta } from '@audio/encode-mp3/meta'
let tagged = writeMeta(mp3Bytes, { meta: { title: 'foo' } })- decode – decode any audio format to raw samples.
- wasm-media-encoders – compact WASM MP3 & Vorbis encoders.
- AudioEncoder – native WebCodecs encoder API.
