@audio/decode-aac 1.4.0 → 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
@@ -37,7 +37,7 @@ dec.free()
37
37
 
38
38
  Whole-file decode. Auto-detects M4A (MP4 container) vs raw ADTS.
39
39
 
40
- ### `decoder(): Promise<AACDecoder>`
40
+ ### `decoder(opts?): Promise<AACDecoder>`
41
41
 
42
42
  Creates a decoder instance for manual control.
43
43
 
@@ -45,6 +45,15 @@ Creates a decoder instance for manual control.
45
45
  - `dec.flush()`: discard buffered partial data and return an empty result.
46
46
  - `dec.free()`: release WASM memory.
47
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.
56
+
48
57
  ### `AudioData`
49
58
 
50
59
  ```ts
@@ -53,7 +62,7 @@ Creates a decoder instance for manual control.
53
62
 
54
63
  ## Formats
55
64
 
56
- - M4A / MP4 with AAC audio (LC and HE-AAC v1/v2 with SBR or PS)
65
+ - M4A / MP4 / MOV with AAC audio (LC and HE-AAC v1/v2 with SBR or PS) — QuickTime sound descriptions v0, v1 and v2
57
66
  - M4A / MP4 with bit-exact pure-JS ALAC decoding at 16, 20, 24, or 32 bits
58
67
  - Raw ADTS streams (.aac)
59
68
 
package/decode-aac.d.ts CHANGED
@@ -4,7 +4,8 @@ export interface AudioData {
4
4
  }
5
5
 
6
6
  interface AACDecoder {
7
- decode(data: Uint8Array | ArrayBuffer): 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,10 +68,15 @@ 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')
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
+ }
77
80
  if (!data || !data.byteLength) return EMPTY
78
81
 
79
82
  let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
@@ -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.4.0",
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