@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 +21 -2
- package/decode-aac.d.ts +33 -3
- package/decode-aac.js +198 -43
- package/package.json +2 -2
- package/src/aac.wasm.js +0 -0
- package/src/aac.wasm.cjs +0 -0
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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 = (
|
|
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
|
-
*
|
|
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
|
-
|
|
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 === '
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 +
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
+
"src/aac.wasm.js",
|
|
31
31
|
"LICENSE",
|
|
32
32
|
"audio.js",
|
|
33
33
|
"audio.d.ts"
|
package/src/aac.wasm.js
ADDED
|
Binary file
|
package/src/aac.wasm.cjs
DELETED
|
Binary file
|