@audio/encode 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/audio-encode.d.ts +106 -0
- package/audio-encode.js +215 -0
- package/meta.js +15 -0
- package/package.json +90 -0
- package/stream.d.ts +1 -0
- package/stream.js +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry Iv.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @audio/encode [](https://github.com/audiojs/encode/actions/workflows/test.js.yml)
|
|
2
|
+
|
|
3
|
+
Encode raw audio samples to any format.<br>
|
|
4
|
+
JS / WASM – no ffmpeg, no native bindings, works in both node and browser.<br>
|
|
5
|
+
Small API, minimal size, near-native performance, stream encoding.
|
|
6
|
+
|
|
7
|
+
[](https://npmjs.org/package/@audio/encode/)
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import encode from '@audio/encode';
|
|
11
|
+
|
|
12
|
+
const buf = await encode.wav(channelData, { sampleRate: 44100 });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
#### Supported formats:
|
|
16
|
+
|
|
17
|
+
| Format | Package | Engine |
|
|
18
|
+
|--------|---------|--------|
|
|
19
|
+
| WAV | [@audio/encode-wav](https://npmjs.com/package/@audio/encode-wav) | JS |
|
|
20
|
+
| MP3 | [@audio/encode-mp3](https://npmjs.com/package/@audio/encode-mp3) | WASM |
|
|
21
|
+
| OGG Vorbis | [@audio/encode-ogg](https://npmjs.com/package/@audio/encode-ogg) | WASM |
|
|
22
|
+
| 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) |
|
|
24
|
+
| 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* |
|
|
26
|
+
| 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>
|
|
31
|
+
|
|
32
|
+
### Whole-file encode
|
|
33
|
+
|
|
34
|
+
Specify the format as method name. Input is _Float32Array[]_ (one per channel), a single _Float32Array_ (mono), or an [AudioBuffer](https://npmjs.com/package/audio-buffer).
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import encode from '@audio/encode';
|
|
38
|
+
|
|
39
|
+
const wav = await encode.wav(channelData, { sampleRate: 44100 });
|
|
40
|
+
const aiff = await encode.aiff(channelData, { sampleRate: 44100 });
|
|
41
|
+
const caf = await encode.caf(channelData, { sampleRate: 44100 });
|
|
42
|
+
const mp3 = await encode.mp3(channelData, { sampleRate: 44100, bitrate: 128 });
|
|
43
|
+
const ogg = await encode.ogg(channelData, { sampleRate: 44100, quality: 5 });
|
|
44
|
+
const flac = await encode.flac(channelData, { sampleRate: 44100 });
|
|
45
|
+
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
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`encode.formats` lists the supported names and `encode.mime` maps each to a MIME type — handy for format-agnostic pipelines.
|
|
52
|
+
|
|
53
|
+
### Chunked encoding
|
|
54
|
+
|
|
55
|
+
Call with just options (no data) to create a streaming encoder:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import encode from '@audio/encode';
|
|
59
|
+
|
|
60
|
+
const enc = await encode.mp3({ sampleRate: 44100, bitrate: 128 });
|
|
61
|
+
|
|
62
|
+
const a = await enc(chunk1); // Uint8Array
|
|
63
|
+
const b = await enc(chunk2);
|
|
64
|
+
const c = await enc(null); // end of stream — flush + free
|
|
65
|
+
|
|
66
|
+
// explicit control: enc.flush(), enc.free()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Streaming
|
|
70
|
+
|
|
71
|
+
Pass an async iterable as data — returns an async generator:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import encode from '@audio/encode'
|
|
75
|
+
|
|
76
|
+
for await (let buf of encode.mp3(audioSource, { sampleRate: 44100, bitrate: 128 })) {
|
|
77
|
+
// buf is Uint8Array
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Works with any async iterable source.
|
|
82
|
+
|
|
83
|
+
### Options
|
|
84
|
+
|
|
85
|
+
| Option | Description | Applies to |
|
|
86
|
+
|--------|-------------|------------|
|
|
87
|
+
| `sampleRate` | Output sample rate (required) | all |
|
|
88
|
+
| `bitrate` | Target bitrate in kbps | mp3, opus, webm, aac |
|
|
89
|
+
| `quality` | Quality 0–10 (VBR) | ogg, mp3 |
|
|
90
|
+
| `channels` | Output channel count | all |
|
|
91
|
+
| `bitDepth` | Bit depth: 16/24/32 (wav), 16/24 (aiff, flac), 16/32 (caf) | wav, aiff, flac, caf |
|
|
92
|
+
| `compression` | FLAC compression level 0–8 | flac |
|
|
93
|
+
| `application` | `'audio'`, `'voip'`, or `'lowdelay'` | opus, webm |
|
|
94
|
+
| `meta` | Tags (see below) | wav, mp3, flac, aiff, ogg, opus |
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### Metadata
|
|
98
|
+
|
|
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 `@audio/encode/meta`:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
import { wav } from '@audio/encode/meta'
|
|
114
|
+
|
|
115
|
+
let out = wav(bytes, {
|
|
116
|
+
meta: { title: 'Hare Krishna', artist: 'Prabhupada', year: '1966' },
|
|
117
|
+
markers: [{ sample: 44100, label: 'verse' }],
|
|
118
|
+
regions: [{ sample: 88200, length: 44100, label: 'chorus' }]
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each codec sub-package also exposes its writer directly:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import { writeMeta } from '@audio/encode-mp3/meta'
|
|
126
|
+
let tagged = writeMeta(mp3Bytes, { meta: { title: 'foo' } })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## See also
|
|
131
|
+
|
|
132
|
+
* [decode](https://github.com/audiojs/decode) – decode any audio format to raw samples.
|
|
133
|
+
* [wasm-media-encoders](https://github.com/arseneyr/wasm-media-encoders) – compact WASM MP3 & Vorbis encoders.
|
|
134
|
+
* [AudioEncoder](https://developer.mozilla.org/en-US/docs/Web/API/AudioEncoder) – native WebCodecs encoder API.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[MIT](LICENSE)
|
|
139
|
+
|
|
140
|
+
<a href="https://github.com/krishnized/license/">ॐ</a>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
type AudioInput = Float32Array[] | Float32Array | { numberOfChannels: number; getChannelData(i: number): Float32Array };
|
|
2
|
+
|
|
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
|
+
|
|
15
|
+
export interface EncodeOptions {
|
|
16
|
+
/** Output sample rate (required). */
|
|
17
|
+
sampleRate: number;
|
|
18
|
+
/** Output channel count. */
|
|
19
|
+
channels?: number;
|
|
20
|
+
/** Target bitrate in kbps (lossy: mp3, opus, webm, aac). */
|
|
21
|
+
bitrate?: number;
|
|
22
|
+
/** Quality 0-10 (VBR, format-specific: ogg, mp3). */
|
|
23
|
+
quality?: number;
|
|
24
|
+
/** Bit depth: 16|24|32 for wav, 16|24 for aiff/flac, 16|32 for caf. */
|
|
25
|
+
bitDepth?: number;
|
|
26
|
+
/** FLAC compression level 0-8. */
|
|
27
|
+
compression?: number;
|
|
28
|
+
/** Opus/WebM application: 'audio', 'voip', 'lowdelay'. */
|
|
29
|
+
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[];
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StreamEncoder {
|
|
40
|
+
/** Encode a chunk of audio. */
|
|
41
|
+
(channelData: AudioInput): Promise<Uint8Array>;
|
|
42
|
+
/** Flush remaining data, finalize, and free resources. */
|
|
43
|
+
(): Promise<Uint8Array>;
|
|
44
|
+
/** Flush without freeing. */
|
|
45
|
+
flush(): Promise<Uint8Array>;
|
|
46
|
+
/** Free resources without flushing. */
|
|
47
|
+
free(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FormatEncoder {
|
|
51
|
+
/** Whole-file encode. */
|
|
52
|
+
(channelData: AudioInput, opts: EncodeOptions): Promise<Uint8Array>;
|
|
53
|
+
/** Chunked encode from async iterable. */
|
|
54
|
+
(source: AsyncIterable<AudioInput>, opts: EncodeOptions): AsyncGenerator<Uint8Array>;
|
|
55
|
+
/** Create streaming encoder. */
|
|
56
|
+
(opts: EncodeOptions): Promise<StreamEncoder>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
declare const encode: {
|
|
60
|
+
/** Whole-file encode. */
|
|
61
|
+
(format: string, channelData: AudioInput, opts: EncodeOptions): Promise<Uint8Array>;
|
|
62
|
+
/** Chunked encode from async iterable. */
|
|
63
|
+
(format: string, source: AsyncIterable<AudioInput>, opts: EncodeOptions): AsyncGenerator<Uint8Array>;
|
|
64
|
+
/** Create streaming encoder. */
|
|
65
|
+
(format: string, opts: EncodeOptions): Promise<StreamEncoder>;
|
|
66
|
+
|
|
67
|
+
wav: FormatEncoder;
|
|
68
|
+
aiff: FormatEncoder;
|
|
69
|
+
caf: FormatEncoder;
|
|
70
|
+
mp3: FormatEncoder;
|
|
71
|
+
ogg: FormatEncoder;
|
|
72
|
+
flac: FormatEncoder;
|
|
73
|
+
opus: 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;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default encode;
|
|
88
|
+
|
|
89
|
+
/** Supported format names. */
|
|
90
|
+
export const formats: string[];
|
|
91
|
+
/** Format → MIME type map. */
|
|
92
|
+
export const mime: Record<string, string>;
|
|
93
|
+
|
|
94
|
+
/** Chunked encode from async iterable. */
|
|
95
|
+
export function encodeChunked(
|
|
96
|
+
source: AsyncIterable<AudioInput>,
|
|
97
|
+
format: string,
|
|
98
|
+
opts: EncodeOptions
|
|
99
|
+
): AsyncGenerator<Uint8Array>;
|
|
100
|
+
|
|
101
|
+
/** Wrap codec callbacks into a StreamEncoder with lifecycle management. */
|
|
102
|
+
export function streamEncoder(
|
|
103
|
+
onEncode: (channels: Float32Array[]) => Uint8Array | Promise<Uint8Array>,
|
|
104
|
+
onFlush?: (() => Uint8Array | Promise<Uint8Array>) | null,
|
|
105
|
+
onFree?: (() => void) | null
|
|
106
|
+
): StreamEncoder;
|
package/audio-encode.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio encoder: whole-file and chunked
|
|
3
|
+
* @module encode-audio
|
|
4
|
+
*
|
|
5
|
+
* let buf = await encode.wav(channelData, { sampleRate: 44100 })
|
|
6
|
+
*
|
|
7
|
+
* for await (let bytes of encode.mp3(source, { sampleRate: 44100 })) { ... }
|
|
8
|
+
*
|
|
9
|
+
* let enc = await encode.mp3({ sampleRate: 44100, bitrate: 128 })
|
|
10
|
+
* let chunk = await enc(channelData)
|
|
11
|
+
* let final = await enc() // flush + free
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const EMPTY = new Uint8Array(0)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encode audio — delegates to format-specific encoder.
|
|
18
|
+
* encode('wav', channelData, { sampleRate }) → Promise<Uint8Array>
|
|
19
|
+
* encode('wav', source(), { sampleRate }) → AsyncGenerator<Uint8Array>
|
|
20
|
+
* encode('wav', { sampleRate }) → Promise<StreamEncoder>
|
|
21
|
+
*/
|
|
22
|
+
function encode(format, data, opts) {
|
|
23
|
+
if (!encode[format]) throw Error('Unknown format: ' + format)
|
|
24
|
+
return encode[format](data, opts)
|
|
25
|
+
}
|
|
26
|
+
export default encode
|
|
27
|
+
|
|
28
|
+
function isAudioData(d) {
|
|
29
|
+
return d instanceof Float32Array || Array.isArray(d) || (d?.getChannelData && d?.numberOfChannels)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encode a stream of PCM chunks to the given format.
|
|
34
|
+
* @param {AsyncIterable<Float32Array[]|Float32Array>} source
|
|
35
|
+
* @param {string} format
|
|
36
|
+
* @param {object} opts - encoder options (sampleRate required)
|
|
37
|
+
* @returns {AsyncGenerator<Uint8Array>}
|
|
38
|
+
*/
|
|
39
|
+
async function* encodeChunked(source, format, opts) {
|
|
40
|
+
let enc = await encode[format](opts)
|
|
41
|
+
try {
|
|
42
|
+
for await (let chunk of source) {
|
|
43
|
+
let buf = await enc(chunk)
|
|
44
|
+
if (buf.length) yield buf
|
|
45
|
+
}
|
|
46
|
+
let final = await enc()
|
|
47
|
+
if (final.length) yield final
|
|
48
|
+
} catch (e) { enc.free(); throw e }
|
|
49
|
+
}
|
|
50
|
+
export { encodeChunked }
|
|
51
|
+
|
|
52
|
+
// --- format registration ---
|
|
53
|
+
|
|
54
|
+
// Lazy-loaded meta writers (per-format), keyed by format name. Loaded on first
|
|
55
|
+
// use when `meta`/`markers`/`regions` is passed to that format's encoder.
|
|
56
|
+
const META_WRITERS = {
|
|
57
|
+
wav: () => import('@audio/encode-wav/meta').then(m => m.writeMeta),
|
|
58
|
+
mp3: () => import('@audio/encode-mp3/meta').then(m => m.writeMeta),
|
|
59
|
+
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),
|
|
62
|
+
}
|
|
63
|
+
|
|
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
|
+
|
|
68
|
+
function reg(name, load) {
|
|
69
|
+
encode[name] = fmt(name, async (opts) => {
|
|
70
|
+
let { meta, markers, regions, ...rest } = opts || {}
|
|
71
|
+
let hasMeta = meta || markers?.length || regions?.length
|
|
72
|
+
let init = (await load()).default
|
|
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)
|
|
76
|
+
return streamEncoder(ch => codec.encode(ch), () => codec.flush(), () => codec.free())
|
|
77
|
+
// Meta requested: buffer encoder output, splice via writeMeta on flush.
|
|
78
|
+
let writeMeta = await META_WRITERS[name]()
|
|
79
|
+
let parts = []
|
|
80
|
+
return streamEncoder(
|
|
81
|
+
async ch => { let b = await codec.encode(ch); if (b?.length) parts.push(b); return EMPTY },
|
|
82
|
+
async () => {
|
|
83
|
+
let f = await codec.flush(); if (f?.length) parts.push(f)
|
|
84
|
+
let total = 0; for (let p of parts) total += p.length
|
|
85
|
+
let all = new Uint8Array(total), off = 0
|
|
86
|
+
for (let p of parts) { all.set(p, off); off += p.length }
|
|
87
|
+
return writeMeta(all, { meta: meta || {}, markers: markers || [], regions: regions || [] })
|
|
88
|
+
},
|
|
89
|
+
() => codec.free(),
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
reg('wav', () => import('@audio/encode-wav'))
|
|
95
|
+
reg('aiff', () => import('@audio/encode-aiff'))
|
|
96
|
+
reg('caf', () => import('@audio/encode-caf'))
|
|
97
|
+
reg('mp3', () => import('@audio/encode-mp3'))
|
|
98
|
+
reg('ogg', () => import('@audio/encode-ogg'))
|
|
99
|
+
reg('flac', () => import('@audio/encode-flac'))
|
|
100
|
+
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
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Wrap a stream factory into whole-file encoder + streaming
|
|
117
|
+
* 1 arg (opts) → streaming encoder function
|
|
118
|
+
* 2 args (data, opts) → whole-file encode
|
|
119
|
+
*/
|
|
120
|
+
function fmt(name, init) {
|
|
121
|
+
let fn = (data, opts) => {
|
|
122
|
+
// 1 arg = streaming: encode.mp3({ sampleRate })
|
|
123
|
+
if (!opts) return init(data)
|
|
124
|
+
// 2 args, async iterable = chunked: encode.mp3(source(), { sampleRate })
|
|
125
|
+
if (data && (typeof data[Symbol.asyncIterator] === 'function' || typeof data[Symbol.iterator] === 'function' && !isAudioData(data)))
|
|
126
|
+
return encodeChunked(data, name, opts)
|
|
127
|
+
// 2 args = whole-file: encode.mp3(channelData, { sampleRate })
|
|
128
|
+
return wholeFile(data, opts, init)
|
|
129
|
+
}
|
|
130
|
+
return fn
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function wholeFile(data, opts, init) {
|
|
134
|
+
if (!opts.sampleRate) throw Error('sampleRate is required')
|
|
135
|
+
let ch = channels(data)
|
|
136
|
+
if (!ch.length || !ch[0].length) return EMPTY
|
|
137
|
+
let enc = await init({ channels: ch.length, ...opts })
|
|
138
|
+
try {
|
|
139
|
+
let result = await enc(ch)
|
|
140
|
+
let flushed = await enc()
|
|
141
|
+
return merge(result, flushed)
|
|
142
|
+
} catch (e) { enc.free(); throw e }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// normalize input to Float32Array[]
|
|
146
|
+
function channels(data) {
|
|
147
|
+
if (!data) return []
|
|
148
|
+
if (Array.isArray(data)) {
|
|
149
|
+
if (data[0] instanceof Float32Array) return data
|
|
150
|
+
return []
|
|
151
|
+
}
|
|
152
|
+
if (data instanceof Float32Array) return [data]
|
|
153
|
+
// AudioBuffer or AudioBuffer-like ({ numberOfChannels, getChannelData })
|
|
154
|
+
if (data.getChannelData && data.numberOfChannels) {
|
|
155
|
+
let ch = []
|
|
156
|
+
for (let i = 0; i < data.numberOfChannels; i++) ch.push(data.getChannelData(i))
|
|
157
|
+
return ch
|
|
158
|
+
}
|
|
159
|
+
return []
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* StreamEncoder — a callable function:
|
|
164
|
+
* enc(channelData) — encode audio, returns Uint8Array
|
|
165
|
+
* enc() — flush + finalize + free
|
|
166
|
+
* enc.flush() — flush without freeing
|
|
167
|
+
* enc.free() — release without flushing
|
|
168
|
+
*/
|
|
169
|
+
export function streamEncoder(onEncode, onFlush, onFree) {
|
|
170
|
+
let done = false
|
|
171
|
+
let fn = async (data) => {
|
|
172
|
+
if (data) {
|
|
173
|
+
if (done) throw Error('Encoder already freed')
|
|
174
|
+
let ch = channels(data)
|
|
175
|
+
try { return norm(await onEncode(ch)) }
|
|
176
|
+
catch (e) { done = true; onFree?.(); throw e }
|
|
177
|
+
}
|
|
178
|
+
// no args = end of stream
|
|
179
|
+
if (done) return EMPTY
|
|
180
|
+
done = true
|
|
181
|
+
try {
|
|
182
|
+
let result = onFlush ? norm(await onFlush()) : EMPTY
|
|
183
|
+
onFree?.()
|
|
184
|
+
return result
|
|
185
|
+
} catch (e) { onFree?.(); throw e }
|
|
186
|
+
}
|
|
187
|
+
fn.flush = async () => {
|
|
188
|
+
if (done) return EMPTY
|
|
189
|
+
return onFlush ? norm(await onFlush()) : EMPTY
|
|
190
|
+
}
|
|
191
|
+
fn.free = () => {
|
|
192
|
+
if (done) return
|
|
193
|
+
done = true
|
|
194
|
+
onFree?.()
|
|
195
|
+
}
|
|
196
|
+
return fn
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ensure Uint8Array
|
|
200
|
+
function norm(r) {
|
|
201
|
+
if (!r?.length) return EMPTY
|
|
202
|
+
if (r instanceof Uint8Array) return r
|
|
203
|
+
if (r.buffer) return new Uint8Array(r.buffer, r.byteOffset, r.byteLength)
|
|
204
|
+
return new Uint8Array(r)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// concat two Uint8Arrays
|
|
208
|
+
function merge(a, b) {
|
|
209
|
+
if (!b?.length) return a || EMPTY
|
|
210
|
+
if (!a?.length) return b || EMPTY
|
|
211
|
+
let out = new Uint8Array(a.length + b.length)
|
|
212
|
+
out.set(a)
|
|
213
|
+
out.set(b, a.length)
|
|
214
|
+
return out
|
|
215
|
+
}
|
package/meta.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta writers — re-export from codec packages.
|
|
3
|
+
* @module encode-audio/meta
|
|
4
|
+
*
|
|
5
|
+
* import { wav, mp3, flac, aiff, ogg } from 'encode-audio/meta'
|
|
6
|
+
* let out = wav(bytes, { meta, markers, regions })
|
|
7
|
+
*
|
|
8
|
+
* Opus bakes tags at encode time — pass `meta` to `encode.opus(...)` directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { writeMeta as wav } from '@audio/encode-wav/meta'
|
|
12
|
+
export { writeMeta as mp3 } from '@audio/encode-mp3/meta'
|
|
13
|
+
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/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/encode",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Encode audio data in node or browser",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "audio-encode.js",
|
|
8
|
+
"types": "audio-encode.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"browser": "./audio-encode.js",
|
|
12
|
+
"node": "./audio-encode.js",
|
|
13
|
+
"default": "./audio-encode.js"
|
|
14
|
+
},
|
|
15
|
+
"./stream": "./stream.js",
|
|
16
|
+
"./meta": "./meta.js",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"audio-encode.js",
|
|
21
|
+
"audio-encode.d.ts",
|
|
22
|
+
"stream.js",
|
|
23
|
+
"stream.d.ts",
|
|
24
|
+
"meta.js",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"workspaces": [
|
|
28
|
+
"packages/*"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node test.js",
|
|
32
|
+
"test:all": "npm test --workspaces && npm test",
|
|
33
|
+
"publish:all": "for d in packages/* .; do id=$(node -p \"require('./$d/package.json').name+'@'+require('./$d/package.json').version\"); npm view \"$id\" version >/dev/null 2>&1 && echo \"skip $id\" || (cd \"$d\" && npm publish); done",
|
|
34
|
+
"build": "npm run build --workspaces --if-present"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/audiojs/encode.git"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"audio",
|
|
42
|
+
"encode",
|
|
43
|
+
"encoder",
|
|
44
|
+
"codec",
|
|
45
|
+
"wav",
|
|
46
|
+
"mp3",
|
|
47
|
+
"ogg",
|
|
48
|
+
"vorbis",
|
|
49
|
+
"opus",
|
|
50
|
+
"flac",
|
|
51
|
+
"aiff",
|
|
52
|
+
"caf",
|
|
53
|
+
"webm",
|
|
54
|
+
"aac",
|
|
55
|
+
"qoa",
|
|
56
|
+
"pcm",
|
|
57
|
+
"streaming"
|
|
58
|
+
],
|
|
59
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/audiojs/encode/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/audiojs/encode#readme",
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@audio/encode-aac": "^1.0.0",
|
|
67
|
+
"@audio/encode-aiff": "^1.1.0",
|
|
68
|
+
"@audio/encode-caf": "^1.0.0",
|
|
69
|
+
"@audio/encode-flac": "^1.1.0",
|
|
70
|
+
"@audio/encode-mp3": "^1.1.0",
|
|
71
|
+
"@audio/encode-ogg": "^1.1.0",
|
|
72
|
+
"@audio/encode-opus": "^1.1.0",
|
|
73
|
+
"@audio/encode-qoa": "^1.0.0",
|
|
74
|
+
"@audio/encode-wav": "^1.2.0",
|
|
75
|
+
"@audio/encode-webm": "^1.0.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"audio-buffer": "^7.3.0",
|
|
79
|
+
"audio-decode": "^4.0.0",
|
|
80
|
+
"audio-lena": "^3.0.0",
|
|
81
|
+
"esbuild": "^0.28.0",
|
|
82
|
+
"tst": "^9.2.2"
|
|
83
|
+
},
|
|
84
|
+
"publishConfig": {
|
|
85
|
+
"access": "public"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18"
|
|
89
|
+
}
|
|
90
|
+
}
|
package/stream.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { encodeChunked as default, encodeChunked } from './audio-encode.js';
|
package/stream.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunked encoder — re-exports encodeChunked from main module.
|
|
3
|
+
* @module encode-audio/stream
|
|
4
|
+
*
|
|
5
|
+
* for await (let bytes of encode.mp3(pcmSource, { sampleRate: 44100 })) { ... }
|
|
6
|
+
*/
|
|
7
|
+
export { encodeChunked as default, encodeChunked } from './audio-encode.js'
|