@audio/decode-aac 1.5.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
@@ -54,6 +54,16 @@ let dec = await decoder({ alac }) // ALAC magic cookie
54
54
 
55
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
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
+
57
67
  ### `AudioData`
58
68
 
59
69
  ```ts
package/decode-aac.d.ts CHANGED
@@ -18,7 +18,29 @@ export interface RawOptions {
18
18
  asc?: Uint8Array;
19
19
  /** ALAC magic cookie (24-byte ALACSpecificConfig, `alac` box body, or full atom) — decode raw ALAC frames */
20
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;
21
23
  }
22
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
+
23
45
  /** Create streaming decoder instance. Auto-detects M4A vs ADTS, or decodes raw frames when a config is given. */
24
46
  export function decoder(opts?: RawOptions): Promise<AACDecoder>;
package/decode-aac.js CHANGED
@@ -40,13 +40,16 @@ export default async function decode(src) {
40
40
  * With `asc` (AudioSpecificConfig) or `alac` (ALAC magic cookie) it decodes raw access
41
41
  * units delivered out-of-band by a container demuxer (MP4, Matroska, AVI): each decode()
42
42
  * call takes one complete frame, or an array of frames.
43
- * @param {{ asc?: Uint8Array, alac?: Uint8Array }} [opts]
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]
44
46
  * @returns {Promise<{decode(chunk: Uint8Array | Uint8Array[]): {channelData, sampleRate}, flush(), free()}>}
45
47
  */
46
48
  export async function decoder(opts) {
47
49
  let dec = new AACDecoder(await getMod())
48
50
  if (opts?.asc) { dec._initASC(opts.asc); dec._raw = true }
49
51
  else if (opts?.alac) { dec._initALAC(opts.alac); dec._raw = true }
52
+ if (dec._raw && opts.gapless) dec._window(opts.gapless)
50
53
  return dec
51
54
  }
52
55
 
@@ -69,13 +72,15 @@ class AACDecoder {
69
72
  this._accumLen = 0
70
73
  this._alac = null // ALAC decoder when the M4A carries Apple Lossless
71
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)
72
77
  }
73
78
 
74
79
  decode(data) {
75
80
  if (this.done) throw Error('Decoder already freed')
76
81
  if (this._raw) {
77
82
  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
83
+ return frames.length ? this._trim(this._feedFrames(frames)) : EMPTY
79
84
  }
80
85
  if (!data || !data.byteLength) return EMPTY
81
86
 
@@ -126,10 +131,12 @@ class AACDecoder {
126
131
  let buf = this._catAccum()
127
132
  // tables are collected per trak — a second non-audio track (e.g. a QuickTime
128
133
  // chapter/text track, github #48) must not clobber the audio track's tables
129
- let traks = [], t = null, moov = false
134
+ let traks = [], t = null, moov = false, mvhd = null, smpb = null
130
135
 
131
136
  parseBoxes(buf, 0, buf.length, (type, data) => {
132
137
  if (type === 'moov') moov = true
138
+ else if (type === 'mvhd') mvhd = data
139
+ else if (type === 'ilst') smpb = data
133
140
  else if (type === 'trak') traks.push(t = {})
134
141
  else if (!t) return
135
142
  else if (type === 'esds') t.asc = parseEsds(data)
@@ -138,10 +145,12 @@ class AACDecoder {
138
145
  else if (type === 'stco') t.stco = parseStco(data)
139
146
  else if (type === 'co64') t.stco = parseCo64(data)
140
147
  else if (type === 'stsc') t.stsc = parseStsc(data)
148
+ else if (type === 'mdhd' || type === 'elst' || type === 'stts') t[type] = data
141
149
  })
142
150
 
143
151
  // the audio track: first trak with an audio config + sample tables
144
- let { asc, alacCookie, stsz, stco, stsc } = traks.find(t => (t.asc || t.alacCookie) && t.stsz && t.stco?.length) ?? {}
152
+ let track = traks.find(t => (t.asc || t.alacCookie) && t.stsz && t.stco?.length) ?? {}
153
+ let { asc, alacCookie, stsz, stco, stsc } = track
145
154
  if ((!asc && !alacCookie) || !stsz || !stco?.length) {
146
155
  // the whole moov is here and still no AAC/ALAC track — other codecs live in @audio/decode-mp4
147
156
  if (moov) throw Error('No AAC/ALAC audio track in MP4')
@@ -150,6 +159,8 @@ class AACDecoder {
150
159
 
151
160
  if (alacCookie) this._initALAC(alacCookie)
152
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)
153
164
 
154
165
  // Streaming: walk sample tables by absolute file offset so chunk boundaries are irrelevant.
155
166
  this._accum = null; this._accumLen = 0
@@ -180,6 +191,8 @@ class AACDecoder {
180
191
  this.ch = m.getValue(chP, 'i8')
181
192
  if (!this.ch) { m._aac_close(h); throw Error('AAC init: no channels in ASC') }
182
193
  this.h = h
194
+ this._core = ascRate(asc) || this.sr
195
+ this._order = AAC_ORDER[(asc[1] >> 3) & 15]
183
196
  }
184
197
 
185
198
  _feedM4AData(buf) {
@@ -218,7 +231,26 @@ class AACDecoder {
218
231
  this._left = null
219
232
  }
220
233
 
221
- 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
222
254
  }
223
255
 
224
256
  _alloc(len) {
@@ -243,6 +275,8 @@ class AACDecoder {
243
275
  m.HEAPU8.set(buf, ptr)
244
276
  let consumed = m._aac_init(h, ptr, buf.length, srP, chP)
245
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)]
246
280
  this.sr = m.getValue(srP, 'i32')
247
281
  this.ch = m.getValue(chP, 'i8')
248
282
  if (!this.ch) {
@@ -301,9 +335,10 @@ class AACDecoder {
301
335
  let channelData = Array.from({ length: channels }, () => new Float32Array(totalPerCh))
302
336
  let pos = 0
303
337
  for (let { data, ch, spc } of chunks) {
338
+ let order = ch === this._order?.length ? this._order : null
304
339
  for (let c = 0; c < ch; c++) {
305
- let out = channelData[c]
306
- 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]
307
342
  }
308
343
  pos += spc
309
344
  }
@@ -331,6 +366,21 @@ class AACDecoder {
331
366
  }
332
367
 
333
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
+
334
384
  // ===== M4A demuxer =====
335
385
 
336
386
  function append(left, buf) {
@@ -388,6 +438,91 @@ function parseSampleDesc(buf, off, len, cb) {
388
438
  }
389
439
  }
390
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
+
391
526
  function parseEsds(data) {
392
527
  let off = 4
393
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.5.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",