@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 +10 -0
- package/decode-aac.d.ts +22 -0
- package/decode-aac.js +142 -7
- package/package.json +1 -1
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
|
-
*
|
|
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
|
|
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 +
|
|
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) {
|