@audio/decode-aac 1.3.4 → 1.5.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/README.md +20 -9
- package/decode-aac.d.ts +12 -4
- package/decode-aac.js +60 -40
- package/package.json +2 -2
- package/src/aac.wasm.js +0 -0
- package/src/aac.wasm.cjs +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @audio/decode-aac
|
|
2
2
|
|
|
3
|
-
Decode AAC/M4A and ALAC
|
|
3
|
+
Decode AAC/M4A and ALAC audio to PCM float samples. FAAD2 WASM handles AAC; a pure-JS port of Apple's reference decoder handles ALAC. The M4A sample entry selects the codec.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ npm i @audio/decode-aac
|
|
|
13
13
|
```js
|
|
14
14
|
import decode from '@audio/decode-aac'
|
|
15
15
|
|
|
16
|
-
// M4A or raw ADTS
|
|
16
|
+
// M4A or raw ADTS; auto-detected
|
|
17
17
|
let { channelData, sampleRate } = await decode(uint8array)
|
|
18
18
|
// channelData: Float32Array[] (one per channel)
|
|
19
19
|
// sampleRate: number
|
|
@@ -29,19 +29,30 @@ let { channelData, sampleRate } = dec.decode(chunk)
|
|
|
29
29
|
dec.free()
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
`decoder()` is asynchronous. Its `decode()` and `flush()` methods are synchronous.
|
|
33
|
+
|
|
32
34
|
## API
|
|
33
35
|
|
|
34
36
|
### `decode(src: Uint8Array | ArrayBuffer): Promise<AudioData>`
|
|
35
37
|
|
|
36
38
|
Whole-file decode. Auto-detects M4A (MP4 container) vs raw ADTS.
|
|
37
39
|
|
|
38
|
-
### `decoder(): Promise<AACDecoder>`
|
|
40
|
+
### `decoder(opts?): Promise<AACDecoder>`
|
|
39
41
|
|
|
40
42
|
Creates a decoder instance for manual control.
|
|
41
43
|
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
44
|
+
- `dec.decode(data)`: decode a `Uint8Array` or `ArrayBuffer` chunk.
|
|
45
|
+
- `dec.flush()`: discard buffered partial data and return an empty result.
|
|
46
|
+
- `dec.free()`: release WASM memory.
|
|
47
|
+
|
|
48
|
+
Without options the byte stream is auto-detected (M4A or ADTS). Container demuxers that hold the codec config out-of-band pass it up-front and feed raw access units — one frame, or an array of frames, per `decode()` call:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
let dec = await decoder({ asc }) // AudioSpecificConfig (esds, Matroska CodecPrivate, WAVEFORMATEX extra bytes)
|
|
52
|
+
let dec = await decoder({ alac }) // ALAC magic cookie
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This is how [@audio/decode-mp4](../decode-mp4), [@audio/decode-webm](../decode-webm) and [@audio/decode-avi](../decode-avi) decode AAC and ALAC tracks from video files.
|
|
45
56
|
|
|
46
57
|
### `AudioData`
|
|
47
58
|
|
|
@@ -51,8 +62,8 @@ Creates a decoder instance for manual control.
|
|
|
51
62
|
|
|
52
63
|
## Formats
|
|
53
64
|
|
|
54
|
-
- M4A / MP4 with AAC audio (LC
|
|
55
|
-
- M4A / MP4 with ALAC
|
|
65
|
+
- M4A / MP4 / MOV with AAC audio (LC and HE-AAC v1/v2 with SBR or PS) — QuickTime sound descriptions v0, v1 and v2
|
|
66
|
+
- M4A / MP4 with bit-exact pure-JS ALAC decoding at 16, 20, 24, or 32 bits
|
|
56
67
|
- Raw ADTS streams (.aac)
|
|
57
68
|
|
|
58
69
|
## Metadata
|
|
@@ -66,4 +77,4 @@ let { meta, sampleRate } = parseMeta(m4aBytes)
|
|
|
66
77
|
|
|
67
78
|
## License
|
|
68
79
|
|
|
69
|
-
AAC decoding
|
|
80
|
+
[ॐ](https://github.com/krishnized/license/) · AAC decoding [GPL-2.0](./LICENSE) (FAAD2), ALAC decoding Apache-2.0 (port of Apple's ALAC reference)
|
package/decode-aac.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
interface AudioData {
|
|
1
|
+
export interface AudioData {
|
|
2
2
|
channelData: Float32Array[];
|
|
3
3
|
sampleRate: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
interface AACDecoder {
|
|
7
|
-
|
|
7
|
+
/** Byte stream (M4A/ADTS), or with `asc`/`alac` options: one raw access unit or an array of them */
|
|
8
|
+
decode(data: Uint8Array | ArrayBuffer | Uint8Array[]): AudioData;
|
|
8
9
|
flush(): AudioData;
|
|
9
10
|
free(): void;
|
|
10
11
|
}
|
|
@@ -12,5 +13,12 @@ interface AACDecoder {
|
|
|
12
13
|
/** Whole-file decode — auto-detects M4A vs ADTS */
|
|
13
14
|
export default function decode(src: ArrayBuffer | Uint8Array): Promise<AudioData>;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
export interface RawOptions {
|
|
17
|
+
/** AudioSpecificConfig — decode raw AAC access units from a container demuxer */
|
|
18
|
+
asc?: Uint8Array;
|
|
19
|
+
/** ALAC magic cookie (24-byte ALACSpecificConfig, `alac` box body, or full atom) — decode raw ALAC frames */
|
|
20
|
+
alac?: Uint8Array;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Create streaming decoder instance. Auto-detects M4A vs ADTS, or decodes raw frames when a config is given. */
|
|
24
|
+
export function decoder(opts?: RawOptions): Promise<AACDecoder>;
|
package/decode-aac.js
CHANGED
|
@@ -7,23 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createALAC } from './alac.js'
|
|
10
|
+
import createAAC from './src/aac.wasm.js'
|
|
10
11
|
|
|
11
12
|
let _modP
|
|
12
13
|
|
|
13
14
|
async function getMod() {
|
|
14
15
|
if (_modP) return _modP
|
|
15
|
-
let p = (
|
|
16
|
-
let createAAC
|
|
17
|
-
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
18
|
-
let m = 'module'
|
|
19
|
-
let { createRequire } = await import(m)
|
|
20
|
-
createAAC = createRequire(import.meta.url)('./src/aac.wasm.cjs')
|
|
21
|
-
} else {
|
|
22
|
-
let mod = await import('./src/aac.wasm.cjs')
|
|
23
|
-
createAAC = mod.default || mod
|
|
24
|
-
}
|
|
25
|
-
return createAAC()
|
|
26
|
-
})()
|
|
16
|
+
let p = createAAC()
|
|
27
17
|
_modP = p
|
|
28
18
|
try { return await p }
|
|
29
19
|
catch (e) { _modP = null; throw e }
|
|
@@ -45,11 +35,19 @@ export default async function decode(src) {
|
|
|
45
35
|
}
|
|
46
36
|
|
|
47
37
|
/**
|
|
48
|
-
* Create decoder instance
|
|
49
|
-
*
|
|
38
|
+
* Create decoder instance.
|
|
39
|
+
* Without options the decoder auto-detects M4A vs ADTS from the byte stream.
|
|
40
|
+
* With `asc` (AudioSpecificConfig) or `alac` (ALAC magic cookie) it decodes raw access
|
|
41
|
+
* units delivered out-of-band by a container demuxer (MP4, Matroska, AVI): each decode()
|
|
42
|
+
* call takes one complete frame, or an array of frames.
|
|
43
|
+
* @param {{ asc?: Uint8Array, alac?: Uint8Array }} [opts]
|
|
44
|
+
* @returns {Promise<{decode(chunk: Uint8Array | Uint8Array[]): {channelData, sampleRate}, flush(), free()}>}
|
|
50
45
|
*/
|
|
51
|
-
export async function decoder() {
|
|
52
|
-
|
|
46
|
+
export async function decoder(opts) {
|
|
47
|
+
let dec = new AACDecoder(await getMod())
|
|
48
|
+
if (opts?.asc) { dec._initASC(opts.asc); dec._raw = true }
|
|
49
|
+
else if (opts?.alac) { dec._initALAC(opts.alac); dec._raw = true }
|
|
50
|
+
return dec
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
const EMPTY = Object.freeze({ channelData: [], sampleRate: 0 })
|
|
@@ -70,11 +68,16 @@ class AACDecoder {
|
|
|
70
68
|
this._accum = null // Uint8Array[] — M4A header accumulator
|
|
71
69
|
this._accumLen = 0
|
|
72
70
|
this._alac = null // ALAC decoder when the M4A carries Apple Lossless
|
|
71
|
+
this._raw = false // raw access units (config given up-front)
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
decode(data) {
|
|
76
75
|
if (this.done) throw Error('Decoder already freed')
|
|
77
|
-
if (
|
|
76
|
+
if (this._raw) {
|
|
77
|
+
let frames = (Array.isArray(data) ? data : [data]).filter(f => f?.byteLength).map(f => f instanceof Uint8Array ? f : new Uint8Array(f))
|
|
78
|
+
return frames.length ? this._feedFrames(frames) : EMPTY
|
|
79
|
+
}
|
|
80
|
+
if (!data || !data.byteLength) return EMPTY
|
|
78
81
|
|
|
79
82
|
let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
|
|
80
83
|
|
|
@@ -123,10 +126,11 @@ class AACDecoder {
|
|
|
123
126
|
let buf = this._catAccum()
|
|
124
127
|
// tables are collected per trak — a second non-audio track (e.g. a QuickTime
|
|
125
128
|
// chapter/text track, github #48) must not clobber the audio track's tables
|
|
126
|
-
let traks = [], t = null
|
|
129
|
+
let traks = [], t = null, moov = false
|
|
127
130
|
|
|
128
131
|
parseBoxes(buf, 0, buf.length, (type, data) => {
|
|
129
|
-
if (type === '
|
|
132
|
+
if (type === 'moov') moov = true
|
|
133
|
+
else if (type === 'trak') traks.push(t = {})
|
|
130
134
|
else if (!t) return
|
|
131
135
|
else if (type === 'esds') t.asc = parseEsds(data)
|
|
132
136
|
else if (type === 'alac') t.alacCookie = data // ALAC magic cookie: version/flags(4) + ALACSpecificConfig(24)
|
|
@@ -138,27 +142,15 @@ class AACDecoder {
|
|
|
138
142
|
|
|
139
143
|
// the audio track: first trak with an audio config + sample tables
|
|
140
144
|
let { asc, alacCookie, stsz, stco, stsc } = traks.find(t => (t.asc || t.alacCookie) && t.stsz && t.stco?.length) ?? {}
|
|
141
|
-
if ((!asc && !alacCookie) || !stsz || !stco?.length)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this._alac = createALAC(alacCookie.subarray(4))
|
|
146
|
-
this.sr = this._alac.config.sampleRate
|
|
147
|
-
this.ch = this._alac.config.numChannels
|
|
148
|
-
} else {
|
|
149
|
-
// Init WASM decoder with ASC
|
|
150
|
-
let m = this.m, h = m._aac_create()
|
|
151
|
-
let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
|
|
152
|
-
let ptr = this._alloc(asc.length)
|
|
153
|
-
m.HEAPU8.set(asc, ptr)
|
|
154
|
-
let err = m._aac_init2(h, ptr, asc.length, srP, chP)
|
|
155
|
-
if (err < 0) { m._aac_close(h); throw Error('M4A init failed (code ' + err + ')') }
|
|
156
|
-
this.sr = m.getValue(srP, 'i32')
|
|
157
|
-
this.ch = m.getValue(chP, 'i8')
|
|
158
|
-
if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
|
|
159
|
-
this.h = h
|
|
145
|
+
if ((!asc && !alacCookie) || !stsz || !stco?.length) {
|
|
146
|
+
// the whole moov is here and still no AAC/ALAC track — other codecs live in @audio/decode-mp4
|
|
147
|
+
if (moov) throw Error('No AAC/ALAC audio track in MP4')
|
|
148
|
+
return EMPTY // moov/tables not ready
|
|
160
149
|
}
|
|
161
150
|
|
|
151
|
+
if (alacCookie) this._initALAC(alacCookie)
|
|
152
|
+
else this._initASC(asc)
|
|
153
|
+
|
|
162
154
|
// Streaming: walk sample tables by absolute file offset so chunk boundaries are irrelevant.
|
|
163
155
|
this._accum = null; this._accumLen = 0
|
|
164
156
|
this._m4a = { sizes: stsz, stco, stsc, idx: 0, ci: 0, sInC: 0, spc: spcAt(0, stsc), nextOff: stco[0] }
|
|
@@ -168,6 +160,28 @@ class AACDecoder {
|
|
|
168
160
|
return this._extractM4A()
|
|
169
161
|
}
|
|
170
162
|
|
|
163
|
+
// ALAC (Apple Lossless) — pure JS, no FAAD2. Cookie: 24-byte ALACSpecificConfig, optionally
|
|
164
|
+
// preceded by version/flags (MP4 `alac` box body, 28) or the whole atom (ffmpeg extradata, 36).
|
|
165
|
+
_initALAC(cookie) {
|
|
166
|
+
this._alac = createALAC(cookie.subarray(cookie.length - 24))
|
|
167
|
+
this.sr = this._alac.config.sampleRate
|
|
168
|
+
this.ch = this._alac.config.numChannels
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Init WASM decoder with AudioSpecificConfig
|
|
172
|
+
_initASC(asc) {
|
|
173
|
+
let m = this.m, h = m._aac_create()
|
|
174
|
+
let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
|
|
175
|
+
let ptr = this._alloc(asc.length)
|
|
176
|
+
m.HEAPU8.set(asc, ptr)
|
|
177
|
+
let err = m._aac_init2(h, ptr, asc.length, srP, chP)
|
|
178
|
+
if (err < 0) { m._aac_close(h); throw Error('AAC init failed (code ' + err + ')') }
|
|
179
|
+
this.sr = m.getValue(srP, 'i32')
|
|
180
|
+
this.ch = m.getValue(chP, 'i8')
|
|
181
|
+
if (!this.ch) { m._aac_close(h); throw Error('AAC init: no channels in ASC') }
|
|
182
|
+
this.h = h
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
_feedM4AData(buf) {
|
|
172
186
|
if (this._skip > 0) {
|
|
173
187
|
let n = Math.min(this._skip, buf.length)
|
|
@@ -350,6 +364,7 @@ function parseBoxes(buf, start, end, cb) {
|
|
|
350
364
|
else if (CONTAINERS.has(type)) {
|
|
351
365
|
if (type === 'trak') cb(type, null) // track boundary — tables that follow belong to this trak
|
|
352
366
|
parseBoxes(buf, bodyOff + (type === 'meta' ? 4 : 0), off + size, cb)
|
|
367
|
+
if (type === 'moov') cb(type, null) // whole moov parsed — tables are final
|
|
353
368
|
}
|
|
354
369
|
else cb(type, buf.subarray(bodyOff, off + size))
|
|
355
370
|
|
|
@@ -362,8 +377,13 @@ function parseSampleDesc(buf, off, len, cb) {
|
|
|
362
377
|
for (let i = 0; i < entries && pos < off + len; i++) {
|
|
363
378
|
let eSize = r32(buf, pos)
|
|
364
379
|
let eType = String.fromCharCode(buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7])
|
|
365
|
-
// recurse into the audio sample entry so its child boxes (esds for AAC, alac cookie for ALAC) surface
|
|
366
|
-
|
|
380
|
+
// recurse into the audio sample entry so its child boxes (esds for AAC, alac cookie for ALAC) surface.
|
|
381
|
+
// QuickTime sound description versions: v0 = 36-byte header, v1 adds 16 bytes, v2 declares its own size.
|
|
382
|
+
if (eType === 'mp4a' || eType === 'alac') {
|
|
383
|
+
let ver = (buf[pos + 16] << 8) | buf[pos + 17]
|
|
384
|
+
let head = ver === 1 ? 52 : ver === 2 ? r32(buf, pos + 36) : 36
|
|
385
|
+
if (eSize > head) parseBoxes(buf, pos + head, pos + eSize, cb)
|
|
386
|
+
}
|
|
367
387
|
pos += eSize
|
|
368
388
|
}
|
|
369
389
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audio/decode-aac",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Decode AAC/M4A (FAAD2 WASM) and ALAC/Apple Lossless (pure JS) to PCM samples",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "decode-aac.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"alac.js",
|
|
28
28
|
"meta.js",
|
|
29
29
|
"decode-aac.d.ts",
|
|
30
|
-
"src/aac.wasm.
|
|
30
|
+
"src/aac.wasm.js",
|
|
31
31
|
"LICENSE",
|
|
32
32
|
"audio.js",
|
|
33
33
|
"audio.d.ts"
|
package/src/aac.wasm.js
ADDED
|
Binary file
|
package/src/aac.wasm.cjs
DELETED
|
Binary file
|