@audio/decode-aac 1.4.0 → 1.6.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,25 @@ 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
+
57
+ ### Gapless
58
+
59
+ An M4A is decoded gapless: the encoder's priming (Apple's encoders: 2112 samples) and padding are trimmed, so the output has the source's length and timing. The track's edit list says where the presentation starts when it has one media edit ([ISO/IEC 14496-12](https://www.iso.org/standard/83102.html) §8.6.6; several are an edit, not a trim: the track decodes whole), else iTunes' `iTunSMPB` tag; FAAD2's own delay (it withholds its first frame; SBR delays HE-AAC 962 samples more) is accounted for. A raw ADTS stream carries no such information and keeps the decoder's delay. A demuxer passes the window it read from its container:
60
+
61
+ ```js
62
+ import { decoder, gapless } from '@audio/decode-aac'
63
+ let dec = await decoder({ asc, gapless: gapless({ elst, mdhd, stts, mvhd, ilst, asc }) }) // box bodies
64
+ // or directly: { gapless: { start, duration } } in seconds of the media timeline
65
+ ```
66
+
48
67
  ### `AudioData`
49
68
 
50
69
  ```ts
@@ -53,7 +72,7 @@ Creates a decoder instance for manual control.
53
72
 
54
73
  ## Formats
55
74
 
56
- - M4A / MP4 with AAC audio (LC and HE-AAC v1/v2 with SBR or PS)
75
+ - M4A / MP4 / MOV with AAC audio (LC and HE-AAC v1/v2 with SBR or PS) — QuickTime sound descriptions v0, v1 and v2
57
76
  - M4A / MP4 with bit-exact pure-JS ALAC decoding at 16, 20, 24, or 32 bits
58
77
  - Raw ADTS streams (.aac)
59
78
 
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,34 @@ 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
+ /** With `asc`/`alac`: trim the encoder's priming and padding to this window, as `gapless()` reads it from the container */
22
+ gapless?: GaplessWindow;
23
+ }
24
+
25
+ /** Seconds of the media timeline: the output starts at `start` and lasts `duration` (to the end when absent) */
26
+ export interface GaplessWindow {
27
+ start: number;
28
+ duration?: number;
29
+ }
30
+
31
+ /** Box bodies of an MP4 audio track (`elst` `mdhd` `stts` `mvhd` `ilst`) and its codec config (`asc` or `alac`) */
32
+ export interface GaplessBoxes {
33
+ elst?: Uint8Array;
34
+ mdhd?: Uint8Array;
35
+ stts?: Uint8Array;
36
+ mvhd?: Uint8Array;
37
+ ilst?: Uint8Array;
38
+ asc?: Uint8Array;
39
+ alac?: Uint8Array;
40
+ }
41
+
42
+ /** The track's gapless window: its edit list's one media edit, else iTunes' iTunSMPB; null without either, or with several media edits */
43
+ export function gapless(boxes: GaplessBoxes): GaplessWindow | null;
44
+
45
+ /** Create streaming decoder instance. Auto-detects M4A vs ADTS, or decodes raw frames when a config is given. */
46
+ 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,22 @@ 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
+ * `gapless` ({ start, duration } seconds of the media timeline, gapless() from the container's boxes)
44
+ * trims the encoder's priming and padding: the output starts at `start` and lasts `duration`.
45
+ * @param {{ asc?: Uint8Array, alac?: Uint8Array, gapless?: { start: number, duration?: number } }} [opts]
46
+ * @returns {Promise<{decode(chunk: Uint8Array | Uint8Array[]): {channelData, sampleRate}, flush(), free()}>}
50
47
  */
51
- export async function decoder() {
52
- return new AACDecoder(await getMod())
48
+ export async function decoder(opts) {
49
+ let dec = new AACDecoder(await getMod())
50
+ if (opts?.asc) { dec._initASC(opts.asc); dec._raw = true }
51
+ else if (opts?.alac) { dec._initALAC(opts.alac); dec._raw = true }
52
+ if (dec._raw && opts.gapless) dec._window(opts.gapless)
53
+ return dec
53
54
  }
54
55
 
55
56
  const EMPTY = Object.freeze({ channelData: [], sampleRate: 0 })
@@ -70,10 +71,17 @@ class AACDecoder {
70
71
  this._accum = null // Uint8Array[] — M4A header accumulator
71
72
  this._accumLen = 0
72
73
  this._alac = null // ALAC decoder when the M4A carries Apple Lossless
74
+ this._raw = false // raw access units (config given up-front)
75
+ this._gap = null // gapless trim: { skip, left } output samples (_window)
76
+ this._core = 0 // the AudioSpecificConfig's own rate (half the output's with SBR)
73
77
  }
74
78
 
75
79
  decode(data) {
76
80
  if (this.done) throw Error('Decoder already freed')
81
+ if (this._raw) {
82
+ let frames = (Array.isArray(data) ? data : [data]).filter(f => f?.byteLength).map(f => f instanceof Uint8Array ? f : new Uint8Array(f))
83
+ return frames.length ? this._trim(this._feedFrames(frames)) : EMPTY
84
+ }
77
85
  if (!data || !data.byteLength) return EMPTY
78
86
 
79
87
  let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
@@ -123,10 +131,13 @@ class AACDecoder {
123
131
  let buf = this._catAccum()
124
132
  // tables are collected per trak — a second non-audio track (e.g. a QuickTime
125
133
  // chapter/text track, github #48) must not clobber the audio track's tables
126
- let traks = [], t = null
134
+ let traks = [], t = null, moov = false, mvhd = null, smpb = null
127
135
 
128
136
  parseBoxes(buf, 0, buf.length, (type, data) => {
129
- if (type === 'trak') traks.push(t = {})
137
+ if (type === 'moov') moov = true
138
+ else if (type === 'mvhd') mvhd = data
139
+ else if (type === 'ilst') smpb = data
140
+ else if (type === 'trak') traks.push(t = {})
130
141
  else if (!t) return
131
142
  else if (type === 'esds') t.asc = parseEsds(data)
132
143
  else if (type === 'alac') t.alacCookie = data // ALAC magic cookie: version/flags(4) + ALACSpecificConfig(24)
@@ -134,31 +145,23 @@ class AACDecoder {
134
145
  else if (type === 'stco') t.stco = parseStco(data)
135
146
  else if (type === 'co64') t.stco = parseCo64(data)
136
147
  else if (type === 'stsc') t.stsc = parseStsc(data)
148
+ else if (type === 'mdhd' || type === 'elst' || type === 'stts') t[type] = data
137
149
  })
138
150
 
139
151
  // the audio track: first trak with an audio config + sample tables
140
- 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
152
+ let track = traks.find(t => (t.asc || t.alacCookie) && t.stsz && t.stco?.length) ?? {}
153
+ let { asc, alacCookie, stsz, stco, stsc } = track
154
+ if ((!asc && !alacCookie) || !stsz || !stco?.length) {
155
+ // the whole moov is here and still no AAC/ALAC track — other codecs live in @audio/decode-mp4
156
+ if (moov) throw Error('No AAC/ALAC audio track in MP4')
157
+ return EMPTY // moov/tables not ready
160
158
  }
161
159
 
160
+ if (alacCookie) this._initALAC(alacCookie)
161
+ else this._initASC(asc)
162
+ let g = gapless({ elst: track.elst, mdhd: track.mdhd, stts: track.stts, mvhd, ilst: smpb, asc, alac: alacCookie })
163
+ if (g) this._window(g)
164
+
162
165
  // Streaming: walk sample tables by absolute file offset so chunk boundaries are irrelevant.
163
166
  this._accum = null; this._accumLen = 0
164
167
  this._m4a = { sizes: stsz, stco, stsc, idx: 0, ci: 0, sInC: 0, spc: spcAt(0, stsc), nextOff: stco[0] }
@@ -168,6 +171,30 @@ class AACDecoder {
168
171
  return this._extractM4A()
169
172
  }
170
173
 
174
+ // ALAC (Apple Lossless) — pure JS, no FAAD2. Cookie: 24-byte ALACSpecificConfig, optionally
175
+ // preceded by version/flags (MP4 `alac` box body, 28) or the whole atom (ffmpeg extradata, 36).
176
+ _initALAC(cookie) {
177
+ this._alac = createALAC(cookie.subarray(cookie.length - 24))
178
+ this.sr = this._alac.config.sampleRate
179
+ this.ch = this._alac.config.numChannels
180
+ }
181
+
182
+ // Init WASM decoder with AudioSpecificConfig
183
+ _initASC(asc) {
184
+ let m = this.m, h = m._aac_create()
185
+ let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
186
+ let ptr = this._alloc(asc.length)
187
+ m.HEAPU8.set(asc, ptr)
188
+ let err = m._aac_init2(h, ptr, asc.length, srP, chP)
189
+ if (err < 0) { m._aac_close(h); throw Error('AAC init failed (code ' + err + ')') }
190
+ this.sr = m.getValue(srP, 'i32')
191
+ this.ch = m.getValue(chP, 'i8')
192
+ if (!this.ch) { m._aac_close(h); throw Error('AAC init: no channels in ASC') }
193
+ this.h = h
194
+ this._core = ascRate(asc) || this.sr
195
+ this._order = AAC_ORDER[(asc[1] >> 3) & 15]
196
+ }
197
+
171
198
  _feedM4AData(buf) {
172
199
  if (this._skip > 0) {
173
200
  let n = Math.min(this._skip, buf.length)
@@ -204,7 +231,26 @@ class AACDecoder {
204
231
  this._left = null
205
232
  }
206
233
 
207
- return frames.length ? this._feedFrames(frames) : EMPTY
234
+ return frames.length ? this._trim(this._feedFrames(frames)) : EMPTY
235
+ }
236
+
237
+ // The presentation window in output samples. FAAD2 withholds its first frame (1024 samples, 2048 with
238
+ // SBR), so its output starts that far into the media; SBR's QMF filterbanks delay it by 962 samples (the
239
+ // offset of Apple's HE-AAC against its source). ALAC has no delay.
240
+ _window({ start = 0, duration }) {
241
+ let sbr = this.sr > this._core * 1.5 ? 2 : 1, offset = this._alac ? 0 : 1024 * sbr - (sbr > 1 ? SBR_DELAY : 0)
242
+ this._gap = { skip: Math.max(0, Math.round(start * this.sr) - offset), left: duration == null ? null : Math.round(duration * this.sr) }
243
+ }
244
+
245
+ // drop the priming, stop at the presentation's end
246
+ _trim(r) {
247
+ let g = this._gap
248
+ if (!g || !r.channelData.length) return r
249
+ let n = r.channelData[0].length, a = Math.min(n, g.skip), b = g.left == null ? n : Math.min(n, a + g.left)
250
+ g.skip -= a
251
+ if (g.left != null) g.left -= b - a
252
+ if (b <= a) return EMPTY
253
+ return a || b < n ? { ...r, channelData: r.channelData.map(c => c.subarray(a, b)) } : r
208
254
  }
209
255
 
210
256
  _alloc(len) {
@@ -229,6 +275,8 @@ class AACDecoder {
229
275
  m.HEAPU8.set(buf, ptr)
230
276
  let consumed = m._aac_init(h, ptr, buf.length, srP, chP)
231
277
  if (consumed < 0) { m._aac_close(h); throw Error('ADTS init failed (code ' + consumed + ')') }
278
+ let a = adtsAt(buf) // the first ADTS header: channel_configuration spans bytes 2 and 3
279
+ if (a >= 0) this._order = AAC_ORDER[((buf[a + 2] & 1) << 2) | (buf[a + 3] >> 6)]
232
280
  this.sr = m.getValue(srP, 'i32')
233
281
  this.ch = m.getValue(chP, 'i8')
234
282
  if (!this.ch) {
@@ -287,9 +335,10 @@ class AACDecoder {
287
335
  let channelData = Array.from({ length: channels }, () => new Float32Array(totalPerCh))
288
336
  let pos = 0
289
337
  for (let { data, ch, spc } of chunks) {
338
+ let order = ch === this._order?.length ? this._order : null
290
339
  for (let c = 0; c < ch; c++) {
291
- let out = channelData[c]
292
- for (let s = 0; s < spc; s++) out[pos + s] = data[s * ch + c]
340
+ let out = channelData[c], k = order ? order[c] : c
341
+ for (let s = 0; s < spc; s++) out[pos + s] = data[s * ch + k]
293
342
  }
294
343
  pos += spc
295
344
  }
@@ -317,6 +366,21 @@ class AACDecoder {
317
366
  }
318
367
 
319
368
 
369
+ // The first ADTS header (ISO/IEC 14496-3 Annex 1.A): past an ID3v2 tag by its size, a 12-bit 0xFFF sync with layer 00,
370
+ // and the next frame's sync where its length points, when the buffer holds it: a 0xFF inside a tag is no header
371
+ function adtsAt(buf) {
372
+ let p = buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33 && buf.length >= 10
373
+ ? 10 + ((buf[6] & 0x7F) << 21 | (buf[7] & 0x7F) << 14 | (buf[8] & 0x7F) << 7 | buf[9] & 0x7F) + (buf[5] & 0x10 ? 10 : 0) : 0
374
+ const sync = i => buf[i] === 0xFF && (buf[i + 1] & 0xF6) === 0xF0
375
+ for (; p + 7 <= buf.length; p++) {
376
+ if (!sync(p)) continue
377
+ let n = p + ((buf[p + 3] & 3) << 11 | buf[p + 4] << 3 | buf[p + 5] >> 5)
378
+ if (n >= p + 7 && (n + 2 > buf.length || sync(n))) return p
379
+ }
380
+ return -1
381
+ }
382
+
383
+
320
384
  // ===== M4A demuxer =====
321
385
 
322
386
  function append(left, buf) {
@@ -350,6 +414,7 @@ function parseBoxes(buf, start, end, cb) {
350
414
  else if (CONTAINERS.has(type)) {
351
415
  if (type === 'trak') cb(type, null) // track boundary — tables that follow belong to this trak
352
416
  parseBoxes(buf, bodyOff + (type === 'meta' ? 4 : 0), off + size, cb)
417
+ if (type === 'moov') cb(type, null) // whole moov parsed — tables are final
353
418
  }
354
419
  else cb(type, buf.subarray(bodyOff, off + size))
355
420
 
@@ -362,12 +427,102 @@ function parseSampleDesc(buf, off, len, cb) {
362
427
  for (let i = 0; i < entries && pos < off + len; i++) {
363
428
  let eSize = r32(buf, pos)
364
429
  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)
430
+ // recurse into the audio sample entry so its child boxes (esds for AAC, alac cookie for ALAC) surface.
431
+ // QuickTime sound description versions: v0 = 36-byte header, v1 adds 16 bytes, v2 declares its own size.
432
+ if (eType === 'mp4a' || eType === 'alac') {
433
+ let ver = (buf[pos + 16] << 8) | buf[pos + 17]
434
+ let head = ver === 1 ? 52 : ver === 2 ? r32(buf, pos + 36) : 36
435
+ if (eSize > head) parseBoxes(buf, pos + head, pos + eSize, cb)
436
+ }
367
437
  pos += eSize
368
438
  }
369
439
  }
370
440
 
441
+ // Gapless: the presentation window of an MP4 audio track, { start, duration } in seconds of the media timeline
442
+ // (null without gapless information). From the edit list (ISO/IEC 14496-12 §8.6.6): its one media edit's media_time
443
+ // is where the presentation starts, past the encoder's priming; its length is the media's (stts) past that
444
+ // start, capped by the edit's duration to within the movie timescale's tick (ffmpeg writes whole milliseconds,
445
+ // floored); a duration of 0 leaves it open (§8.6.6: the edit runs to the media's end). Else iTunes' iTunSMPB tag (Apple's encoders): priming and length in samples at the codec's own rate
446
+ // (the AudioSpecificConfig's, half the output's with SBR; ALAC's cookie's). The codec then drops its own delay
447
+ // (decoder({ gapless })). Boxes are their bodies; `asc` or `alac` is the track's codec config.
448
+ export function gapless({ elst, mdhd, stts, mvhd, ilst, asc, alac }) {
449
+ let rate = asc ? ascRate(asc) : alac?.length >= 24 ? r32(alac, alac.length - 4) : 0
450
+ let edits = mediaEdits(elst), edit = edits[0], ts = mdhd ? timescale(mdhd) : 0
451
+ // several media edits cut or repeat the media: no window of it presents them, so it decodes whole
452
+ if (edits.length > 1) return null
453
+ if (edit && ts) {
454
+ // a fragmented file's tables are empty (its samples ride in moof boxes): its length stays open
455
+ let start = edit.mediaTime / ts, media = stts ? sttsTotal(stts) / ts : 0, duration = media > start ? media - start : undefined
456
+ let mts = mvhd ? timescale(mvhd) : 0
457
+ if (edit.duration && mts) duration = Math.min(duration ?? Infinity, (edit.duration + (mts < ts ? 1 : 0)) / mts)
458
+ return { start, duration }
459
+ }
460
+ let smpb = ilst && parseSmpb(ilst)
461
+ if (smpb && rate) return { start: smpb.priming / rate, duration: smpb.total ? smpb.total / rate : undefined }
462
+ return null
463
+ }
464
+
465
+ const SBR_DELAY = 962
466
+
467
+ // the edits that map media (media_time −1 is an empty edit: a pause, not a trim)
468
+ function mediaEdits(data) {
469
+ let edits = []
470
+ if (!data) return edits
471
+ let v = data[0], n = r32(data, 4), p = 8
472
+ for (let i = 0; i < n; i++, p += v === 1 ? 20 : 12) {
473
+ let duration = v === 1 ? r32(data, p) * 2 ** 32 + r32(data, p + 4) : r32(data, p)
474
+ let hi = r32(data, v === 1 ? p + 8 : p + 4), mediaTime = v === 1 ? (hi | 0) * 2 ** 32 + r32(data, p + 12) : hi | 0
475
+ if (mediaTime >= 0) edits.push({ duration, mediaTime })
476
+ }
477
+ return edits
478
+ }
479
+
480
+ const timescale = d => r32(d, d[0] === 1 ? 20 : 12) // mvhd / mdhd: version(1) flags(3) created modified timescale
481
+
482
+ function sttsTotal(d) {
483
+ let n = r32(d, 4), s = 0
484
+ for (let i = 0; i < n; i++) s += r32(d, 8 + i * 8) * r32(d, 12 + i * 8)
485
+ return s
486
+ }
487
+
488
+ // AAC channel configurations 3–6 put the centre first: C L R [Cs | Ls Rs [LFE]] (ISO/IEC 14496-3 Table 1.19).
489
+ // Output channel j takes decoded channel AAC_ORDER[config][j]: SMPTE/WAV order (L R C LFE Ls Rs), as the family's
490
+ // other decoders and `audio`'s loudness weights read them (ITU-R BS.1770)
491
+ const AAC_ORDER = { 3: [1, 2, 0], 4: [1, 2, 0, 3], 5: [1, 2, 0, 3, 4], 6: [1, 2, 0, 5, 3, 4] }
492
+ // 7 (8 channels) is not mapped: its second pair is outside front by that table, the sides to most 7.1 encoders
493
+
494
+ // the AudioSpecificConfig's own sample rate (the core's, when SBR doubles the output)
495
+ const ASC_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]
496
+ function ascRate(asc) {
497
+ let i = ((asc[0] & 7) << 1) | (asc[1] >> 7)
498
+ if ((asc[0] >> 3) === 31) i = (asc[1] >> 1) & 15 // escaped object type: the index sits 6 bits later
499
+ return i === 15 ? ((asc[1] & 0x7F) << 17 | asc[2] << 9 | asc[3] << 1 | asc[4] >> 7) : ASC_RATES[i] || 0
500
+ }
501
+
502
+ // iTunes gapless tag (freeform '----' atom named iTunSMPB): hex fields, the 2nd priming, 3rd padding, 4th length
503
+ function parseSmpb(ilst) {
504
+ for (let off = 0; off + 8 <= ilst.length;) {
505
+ let size = r32(ilst, off)
506
+ if (size < 8) break
507
+ if (ilst[off + 4] === 0x2D && ilst[off + 5] === 0x2D && ilst[off + 6] === 0x2D && ilst[off + 7] === 0x2D) {
508
+ let name = '', value = null
509
+ for (let p = off + 8; p + 8 <= off + size;) {
510
+ let sz = r32(ilst, p), ty = String.fromCharCode(ilst[p + 4], ilst[p + 5], ilst[p + 6], ilst[p + 7])
511
+ if (sz < 8) break
512
+ if (ty === 'name') name = String.fromCharCode(...ilst.subarray(p + 12, p + sz))
513
+ else if (ty === 'data') value = String.fromCharCode(...ilst.subarray(p + 16, p + sz))
514
+ p += sz
515
+ }
516
+ if (name === 'iTunSMPB' && value) {
517
+ let f = value.trim().split(/\s+/).map(h => parseInt(h, 16))
518
+ if (f.length >= 4 && f.every(Number.isFinite)) return { priming: f[1], padding: f[2], total: f[3] }
519
+ }
520
+ }
521
+ off += size
522
+ }
523
+ return null
524
+ }
525
+
371
526
  function parseEsds(data) {
372
527
  let off = 4
373
528
  while (off < data.length - 2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audio/decode-aac",
3
- "version": "1.4.0",
3
+ "version": "1.6.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