@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @audio/decode-aac
2
2
 
3
- Decode AAC/M4A and ALAC (Apple Lossless) audio to PCM float samples. AAC via FAAD2 (WASM); ALAC via a pure-JS port of Apple's reference decoder — auto-detected from the M4A sample entry. Works in Node.js and browsers, no native dependencies.
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 — auto-detected
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
- - **`dec.decode(data)`** — decode chunk, returns `{ channelData, sampleRate }`
43
- - **`dec.flush()`** — flush remaining (returns empty for AAC)
44
- - **`dec.free()`** — release WASM memory
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, HE-AAC v1/v2 — SBR, PS)
55
- - M4A / MP4 with ALAC (Apple Lossless), 16/20/24/32-bit — pure JS, bit-exact
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: GPL-2.0 (FAAD2). ALAC decoding: Apache-2.0 (port of Apple's ALAC reference). — [krishnized](https://github.com/krishnized/license)
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
- decode(data: Uint8Array): AudioData;
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
- /** Create streaming decoder instance */
16
- export function decoder(): Promise<AACDecoder>;
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 = (async () => {
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
- * @returns {Promise<{decode(chunk: Uint8Array): {channelData, sampleRate}, flush(), free()}>}
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
- return new AACDecoder(await getMod())
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 (!data?.length) return EMPTY
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 === 'trak') traks.push(t = {})
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) return EMPTY // moov/tables not ready
142
-
143
- if (alacCookie) {
144
- // ALAC (Apple Lossless) — pure JS, no FAAD2
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
- if ((eType === 'mp4a' || eType === 'alac') && eSize > 36) parseBoxes(buf, pos + 36, pos + eSize, cb)
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.4",
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.cjs",
30
+ "src/aac.wasm.js",
31
31
  "LICENSE",
32
32
  "audio.js",
33
33
  "audio.d.ts"
Binary file
package/src/aac.wasm.cjs DELETED
Binary file