@audio/decode-aac 1.0.2

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 ADDED
@@ -0,0 +1,9 @@
1
+ This work is offered to Krishna (https://github.com/krishnized/license).
2
+
3
+ ---
4
+
5
+ This package is licensed under the GNU General Public License v2.0 (GPL-2.0),
6
+ as required by the included FAAD2 library.
7
+
8
+ FAAD2 Copyright (C) 2003-2005 M. Bakker, Nero AG, http://www.nero.com
9
+ Full GPL-2.0 text: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # decode-aac
2
+
3
+ Decode AAC/M4A audio to PCM float samples. FAAD2 compiled to WASM — works in Node.js and browsers, no native dependencies.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm i @audio/aac-decode
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import decode from '@audio/aac-decode'
15
+
16
+ // M4A or raw ADTS — auto-detected
17
+ let { channelData, sampleRate } = await decode(uint8array)
18
+ // channelData: Float32Array[] (one per channel)
19
+ // sampleRate: number
20
+ ```
21
+
22
+ ### Streaming
23
+
24
+ ```js
25
+ import { decoder } from '@audio/aac-decode'
26
+
27
+ let dec = await decoder()
28
+ let { channelData, sampleRate } = dec.decode(chunk)
29
+ dec.free()
30
+ ```
31
+
32
+ ## API
33
+
34
+ ### `decode(src: Uint8Array | ArrayBuffer): Promise<AudioData>`
35
+
36
+ Whole-file decode. Auto-detects M4A (MP4 container) vs raw ADTS.
37
+
38
+ ### `decoder(): Promise<AACDecoder>`
39
+
40
+ Creates a decoder instance for manual control.
41
+
42
+ - **`dec.decode(data)`** — decode chunk, returns `{ channelData, sampleRate }`
43
+ - **`dec.flush()`** — flush remaining (returns empty for AAC)
44
+ - **`dec.free()`** — release WASM memory
45
+
46
+ ### `AudioData`
47
+
48
+ ```ts
49
+ { channelData: Float32Array[], sampleRate: number }
50
+ ```
51
+
52
+ ## Formats
53
+
54
+ - M4A / MP4 with AAC audio
55
+ - Raw ADTS streams (.aac)
56
+ - LC, HE-AAC v1/v2 (SBR, PS)
57
+
58
+ ## License
59
+
60
+ GPL-2.0 (FAAD2) — [krishnized](https://github.com/krishnized/license)
@@ -0,0 +1,16 @@
1
+ interface AudioData {
2
+ channelData: Float32Array[];
3
+ sampleRate: number;
4
+ }
5
+
6
+ interface AACDecoder {
7
+ decode(data: Uint8Array): AudioData;
8
+ flush(): AudioData;
9
+ free(): void;
10
+ }
11
+
12
+ /** Whole-file decode — auto-detects M4A vs ADTS */
13
+ export default function decode(src: ArrayBuffer | Uint8Array): Promise<AudioData>;
14
+
15
+ /** Create streaming decoder instance */
16
+ export function decoder(): Promise<AACDecoder>;
package/decode-aac.js ADDED
@@ -0,0 +1,356 @@
1
+ /**
2
+ * AAC decoder — FAAD2 compiled to WASM
3
+ * Decodes M4A (MP4/AAC) and raw ADTS streams
4
+ *
5
+ * let { channelData, sampleRate } = await decode(m4abuf)
6
+ */
7
+
8
+ let _modP
9
+
10
+ async function getMod() {
11
+ if (_modP) return _modP
12
+ let p = (async () => {
13
+ let createAAC
14
+ if (typeof process !== 'undefined' && process.versions?.node) {
15
+ let m = 'module'
16
+ let { createRequire } = await import(m)
17
+ createAAC = createRequire(import.meta.url)('./src/aac.wasm.cjs')
18
+ } else {
19
+ let mod = await import('./src/aac.wasm.cjs')
20
+ createAAC = mod.default || mod
21
+ }
22
+ return createAAC()
23
+ })()
24
+ _modP = p
25
+ try { return await p }
26
+ catch (e) { _modP = null; throw e }
27
+ }
28
+
29
+ /**
30
+ * Whole-file decode
31
+ * @param {Uint8Array|ArrayBuffer} src
32
+ * @returns {Promise<{channelData: Float32Array[], sampleRate: number}>}
33
+ */
34
+ export default async function decode(src) {
35
+ let buf = src instanceof Uint8Array ? src : new Uint8Array(src)
36
+ let dec = await decoder()
37
+ try {
38
+ return dec.decode(buf)
39
+ } finally {
40
+ dec.free()
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Create decoder instance
46
+ * @returns {Promise<{decode(chunk: Uint8Array): {channelData, sampleRate}, flush(), free()}>}
47
+ */
48
+ export async function decoder() {
49
+ return new AACDecoder(await getMod())
50
+ }
51
+
52
+ const EMPTY = Object.freeze({ channelData: [], sampleRate: 0 })
53
+
54
+ class AACDecoder {
55
+ constructor(mod) {
56
+ this.m = mod
57
+ this.h = null
58
+ this.sr = 0
59
+ this.ch = 0
60
+ this.done = false
61
+ this._ptr = 0
62
+ this._cap = 0
63
+ this._left = null
64
+ }
65
+
66
+ decode(data) {
67
+ if (this.done) throw Error('Decoder already freed')
68
+ if (!data?.length) return EMPTY
69
+
70
+ let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
71
+
72
+ // detect M4A (ftyp box at offset 4)
73
+ if (buf.length > 8 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70)
74
+ return this._decodeM4A(buf)
75
+
76
+ return this._decodeADTS(buf)
77
+ }
78
+
79
+ flush() {
80
+ if (this._left) this._left = null
81
+ return EMPTY
82
+ }
83
+
84
+ free() {
85
+ if (this.done) return
86
+ this.done = true
87
+ if (this.h) {
88
+ this.m._aac_close(this.h)
89
+ this.m._aac_free_buf()
90
+ this.h = null
91
+ }
92
+ if (this._ptr) {
93
+ this.m._free(this._ptr)
94
+ this._ptr = 0
95
+ this._cap = 0
96
+ }
97
+ }
98
+
99
+ _alloc(len) {
100
+ if (len > this._cap) {
101
+ if (this._ptr) this.m._free(this._ptr)
102
+ this._cap = len
103
+ this._ptr = this.m._malloc(len)
104
+ }
105
+ return this._ptr
106
+ }
107
+
108
+ _decodeADTS(buf) {
109
+ let m = this.m
110
+
111
+ // prepend leftover from previous call
112
+ if (this._left) {
113
+ let merged = new Uint8Array(this._left.length + buf.length)
114
+ merged.set(this._left)
115
+ merged.set(buf, this._left.length)
116
+ buf = merged
117
+ this._left = null
118
+ }
119
+
120
+ if (!this.h) {
121
+ if (buf.length < 7) { this._left = buf.slice(); return EMPTY }
122
+ let h = m._aac_create()
123
+ let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
124
+ let ptr = this._alloc(buf.length)
125
+ m.HEAPU8.set(buf, ptr)
126
+ let consumed = m._aac_init(h, ptr, buf.length, srP, chP)
127
+ if (consumed < 0) { m._aac_close(h); throw Error('ADTS init failed (code ' + consumed + ')') }
128
+ this.sr = m.getValue(srP, 'i32')
129
+ this.ch = m.getValue(chP, 'i8')
130
+ if (!this.ch) {
131
+ // not enough data to detect channels — buffer for next call
132
+ m._aac_close(h)
133
+ this._left = buf.length < 8192 ? buf.slice() : null
134
+ return EMPTY
135
+ }
136
+ this.h = h
137
+ buf = buf.subarray(consumed)
138
+ }
139
+
140
+ // extract complete ADTS frames only — never feed partial data to FAAD2
141
+ let frames = [], pos = 0
142
+ while (pos + 6 < buf.length) {
143
+ if (buf[pos] !== 0xFF || (buf[pos + 1] & 0xF6) !== 0xF0) { pos++; continue }
144
+ let flen = ((buf[pos + 3] & 0x03) << 11) | (buf[pos + 4] << 3) | (buf[pos + 5] >> 5)
145
+ if (flen < 7 || pos + flen > buf.length) break
146
+ frames.push(buf.subarray(pos, pos + flen))
147
+ pos += flen
148
+ }
149
+
150
+ if (pos < buf.length) {
151
+ let left = buf.subarray(pos)
152
+ this._left = left.length < 8192 ? left.slice() : null
153
+ }
154
+
155
+ if (!frames.length) return EMPTY
156
+ return this._feedFrames(frames)
157
+ }
158
+
159
+ _decodeM4A(buf) {
160
+ let { asc, frames } = demuxM4A(buf)
161
+ if (!asc || !frames.length) return EMPTY
162
+
163
+ let m = this.m
164
+ let h = m._aac_create()
165
+
166
+ let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
167
+ let ptr = this._alloc(asc.length)
168
+ m.HEAPU8.set(asc, ptr)
169
+ let err = m._aac_init2(h, ptr, asc.length, srP, chP)
170
+ if (err < 0) { m._aac_close(h); throw Error('M4A init failed (code ' + err + ')') }
171
+
172
+ this.sr = m.getValue(srP, 'i32')
173
+ this.ch = m.getValue(chP, 'i8')
174
+ if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
175
+ this.h = h
176
+
177
+ return this._feedFrames(frames)
178
+ }
179
+
180
+ _feedFrames(frames) {
181
+ let m = this.m, h = this.h
182
+ let chunks = [], totalPerCh = 0, channels = this.ch, errors = 0
183
+
184
+ for (let frame of frames) {
185
+ let ptr = this._alloc(frame.length)
186
+ m.HEAPU8.set(frame, ptr)
187
+ let out = m._aac_decode(h, ptr, frame.length)
188
+ if (!out) { errors++; continue }
189
+
190
+ let n = m._aac_samples()
191
+ let sr = m._aac_samplerate()
192
+ if (sr) this.sr = sr
193
+ let ch = m._aac_channels()
194
+ if (ch) channels = ch
195
+
196
+ let spc = n / channels
197
+ chunks.push({ data: new Float32Array(m.HEAPF32.buffer, out, n).slice(), ch: channels, spc })
198
+ totalPerCh += spc
199
+ }
200
+
201
+ if (!totalPerCh) return EMPTY
202
+
203
+ let channelData = Array.from({ length: channels }, () => new Float32Array(totalPerCh))
204
+ let pos = 0
205
+ for (let { data, ch, spc } of chunks) {
206
+ for (let c = 0; c < ch; c++) {
207
+ let out = channelData[c]
208
+ for (let s = 0; s < spc; s++) out[pos + s] = data[s * ch + c]
209
+ }
210
+ pos += spc
211
+ }
212
+
213
+ return { channelData, sampleRate: this.sr, errors }
214
+ }
215
+ }
216
+
217
+
218
+ // ===== M4A demuxer =====
219
+
220
+ function demuxM4A(buf) {
221
+ let asc = null, stsz = null, stco = null, stsc = null
222
+ let mdatOff = 0, mdatLen = 0
223
+
224
+ parseBoxes(buf, 0, buf.length, (type, data, off) => {
225
+ if (type === 'esds') asc = parseEsds(data)
226
+ else if (type === 'stsz') stsz = parseStsz(data)
227
+ else if (type === 'stco') stco = parseStco(data)
228
+ else if (type === 'co64') stco = parseCo64(data)
229
+ else if (type === 'stsc') stsc = parseStsc(data)
230
+ else if (type === 'mdat') { mdatOff = off; mdatLen = data.length }
231
+ })
232
+
233
+ if (!asc) return { asc: null, frames: [] }
234
+
235
+ let frames = (stsz && stco)
236
+ ? extractFrames(buf, stsz, stco, stsc)
237
+ : mdatLen ? scanMdat(buf, mdatOff, mdatLen) : []
238
+
239
+ return { asc, frames }
240
+ }
241
+
242
+ const CONTAINERS = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl', 'udta', 'meta', 'edts', 'sinf'])
243
+
244
+ function parseBoxes(buf, start, end, cb) {
245
+ let off = start
246
+ while (off < end - 8) {
247
+ let size = r32(buf, off)
248
+ let type = String.fromCharCode(buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7])
249
+
250
+ if (size === 0) {
251
+ size = end - off
252
+ } else if (size === 1 && off + 16 <= end) {
253
+ size = r32(buf, off + 12)
254
+ if (size < 16) break
255
+ } else if (size < 8) {
256
+ break
257
+ }
258
+ if (off + size > end) size = end - off
259
+
260
+ let bodyOff = off + 8
261
+
262
+ if (type === 'stsd') parseSampleDesc(buf, bodyOff, size - 8, cb)
263
+ else if (CONTAINERS.has(type)) parseBoxes(buf, bodyOff + (type === 'meta' ? 4 : 0), off + size, cb)
264
+ else cb(type, buf.subarray(bodyOff, off + size), bodyOff)
265
+
266
+ off += size
267
+ }
268
+ }
269
+
270
+ function parseSampleDesc(buf, off, len, cb) {
271
+ let entries = r32(buf, off + 4), pos = off + 8
272
+ for (let i = 0; i < entries && pos < off + len; i++) {
273
+ let eSize = r32(buf, pos)
274
+ let eType = String.fromCharCode(buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7])
275
+ if (eType === 'mp4a' && eSize > 36) parseBoxes(buf, pos + 36, pos + eSize, cb)
276
+ pos += eSize
277
+ }
278
+ }
279
+
280
+ function parseEsds(data) {
281
+ let off = 4
282
+ while (off < data.length - 2) {
283
+ let tag = data[off++], len = 0, b
284
+ do { b = data[off++]; len = (len << 7) | (b & 0x7f) } while (b & 0x80 && off < data.length)
285
+ if (tag === 0x03) off += 3
286
+ else if (tag === 0x04) off += 13
287
+ else if (tag === 0x05) return data.subarray(off, off + len)
288
+ else off += len
289
+ }
290
+ return null
291
+ }
292
+
293
+ function parseStsz(data) {
294
+ let sz = r32(data, 4), n = r32(data, 8)
295
+ if (sz) return Array(n).fill(sz)
296
+ let sizes = new Array(n)
297
+ for (let i = 0; i < n; i++) sizes[i] = r32(data, 12 + i * 4)
298
+ return sizes
299
+ }
300
+
301
+ function parseStco(data) {
302
+ let n = r32(data, 4), o = new Array(n)
303
+ for (let i = 0; i < n; i++) o[i] = r32(data, 8 + i * 4)
304
+ return o
305
+ }
306
+
307
+ function parseCo64(data) {
308
+ let n = r32(data, 4), o = new Array(n)
309
+ for (let i = 0; i < n; i++) o[i] = r32(data, 8 + i * 8 + 4)
310
+ return o
311
+ }
312
+
313
+ function parseStsc(data) {
314
+ let n = r32(data, 4), e = new Array(n)
315
+ for (let i = 0; i < n; i++) e[i] = { first: r32(data, 8 + i * 12), spc: r32(data, 12 + i * 12) }
316
+ return e
317
+ }
318
+
319
+ function extractFrames(buf, stsz, stco, stsc) {
320
+ let frames = [], si = 0
321
+ for (let ci = 0; ci < stco.length; ci++) {
322
+ let spc = 1
323
+ if (stsc?.length) {
324
+ let cn = ci + 1
325
+ for (let j = stsc.length - 1; j >= 0; j--)
326
+ if (cn >= stsc[j].first) { spc = stsc[j].spc; break }
327
+ }
328
+ let off = stco[ci]
329
+ for (let s = 0; s < spc && si < stsz.length; s++) {
330
+ let sz = stsz[si++]
331
+ if (off + sz <= buf.length) frames.push(buf.subarray(off, off + sz))
332
+ off += sz
333
+ }
334
+ }
335
+ return frames
336
+ }
337
+
338
+ function scanMdat(buf, off, len) {
339
+ let frames = [], end = off + len, pos = off
340
+ while (pos < end - 7) {
341
+ if (buf[pos] === 0xFF && (buf[pos + 1] & 0xF6) === 0xF0) {
342
+ let flen = ((buf[pos + 3] & 0x03) << 11) | (buf[pos + 4] << 3) | (buf[pos + 5] >> 5)
343
+ if (flen > 0 && pos + flen <= end) {
344
+ frames.push(buf.subarray(pos, pos + flen))
345
+ pos += flen
346
+ continue
347
+ }
348
+ }
349
+ pos++
350
+ }
351
+ return frames
352
+ }
353
+
354
+ function r32(buf, off) {
355
+ return (buf[off] << 24 | buf[off + 1] << 16 | buf[off + 2] << 8 | buf[off + 3]) >>> 0
356
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@audio/decode-aac",
3
+ "version": "1.0.2",
4
+ "description": "Decode AAC/M4A audio via FAAD2 WASM",
5
+ "type": "module",
6
+ "main": "decode-aac.js",
7
+ "types": "decode-aac.d.ts",
8
+ "exports": {
9
+ ".": "./decode-aac.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "bash build.sh",
17
+ "test": "node test.js"
18
+ },
19
+ "files": [
20
+ "decode-aac.js",
21
+ "decode-aac.d.ts",
22
+ "src/aac.wasm.cjs",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "aac",
27
+ "m4a",
28
+ "mp4",
29
+ "audio",
30
+ "decode",
31
+ "decoder",
32
+ "wasm",
33
+ "faad2"
34
+ ],
35
+ "license": "GPL-2.0",
36
+ "author": "audiojs",
37
+ "homepage": "https://github.com/audiojs/decode-aac#readme",
38
+ "bugs": "https://github.com/audiojs/decode-aac/issues",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/audiojs/decode-aac.git"
42
+ },
43
+ "engines": {
44
+ "node": ">=16"
45
+ }
46
+ }
Binary file