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