@audio/decode-mp4 1.2.0 → 1.3.1
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/decode-mp4.js +52 -17
- package/package.json +3 -3
package/decode-mp4.js
CHANGED
|
@@ -238,16 +238,17 @@ function parseStsd(buf, off, end, cb) {
|
|
|
238
238
|
|
|
239
239
|
/** Parse the first audio track out of a complete moov. Returns null while moov is incomplete or absent. */
|
|
240
240
|
function parseTrack(buf) {
|
|
241
|
-
let traks = [], t = null, moov = false, trex = new Map()
|
|
241
|
+
let traks = [], t = null, moov = false, trex = new Map(), movie = {}
|
|
242
242
|
walk(buf, 0, buf.length, (type, data, off) => {
|
|
243
243
|
if (type === 'moov') moov = true
|
|
244
|
+
else if (type === 'mvhd' || type === 'ilst' && !t) movie[type] = data // movie timescale; iTunes tags
|
|
244
245
|
else if (type === 'trak') traks.push(t = { children: {} })
|
|
245
246
|
else if (type === '/trak') t = null
|
|
246
247
|
else if (type === 'trex') trex.set(r32(data, 4), { size: r32(data, 16) })
|
|
247
248
|
else if (!t) return
|
|
248
249
|
else if (type === 'tkhd') t.id = r32(data, data[0] === 1 ? 20 : 12)
|
|
249
250
|
else if (type === 'hdlr') t.handler = str4(data, 8)
|
|
250
|
-
else if (type === 'mdhd') t.timescale = r32(data, data[0] === 1 ? 20 : 12)
|
|
251
|
+
else if (type === 'mdhd') { t.timescale = r32(data, data[0] === 1 ? 20 : 12); t.children.mdhd = data }
|
|
251
252
|
else if (type === 'entry') t.entry ??= data
|
|
252
253
|
else if (type === 'stsz') t.sizes = parseStsz(data)
|
|
253
254
|
else if (type === 'stco') t.stco = parseStco(data)
|
|
@@ -262,6 +263,7 @@ function parseTrack(buf) {
|
|
|
262
263
|
if (trex.size && !track.sizes?.length) { track.fragmented = true; track.trex = trex.get(track.id) || {} }
|
|
263
264
|
else if (!track.sizes || !track.stco?.length) throw Error('Audio track has no sample tables')
|
|
264
265
|
if (!track.entry.sampleRate && track.timescale) track.entry.sampleRate = track.timescale
|
|
266
|
+
track.movie = movie
|
|
265
267
|
return track
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -316,32 +318,34 @@ const MP3_OTI = new Set([0x69, 0x6B]), AAC_OTI = new Set([0x40, 0x66, 0x67, 0x68
|
|
|
316
318
|
const UNSUPPORTED = { 0xE1: 'QCELP' }
|
|
317
319
|
const DTS_TYPES = new Set(['dtsc', 'dtsh', 'dtsl', 'dtse']) // DTS-HD variants carry a decodable core
|
|
318
320
|
|
|
319
|
-
async function createCodec({ entry, children }) {
|
|
321
|
+
async function createCodec({ entry, children, movie }) {
|
|
320
322
|
let { type } = entry
|
|
323
|
+
// AAC and ALAC trim the encoder's priming and padding by the track's edit list or iTunSMPB, MP3 and Opus by the edit list (gapless)
|
|
324
|
+
let boxes = { elst: children.elst, mdhd: children.mdhd, stts: children.stts, mvhd: movie?.mvhd, ilst: movie?.ilst ?? children.ilst }
|
|
321
325
|
if (type === 'mp4a') {
|
|
322
326
|
let { oti, dsi } = children.esds ? parseEsds(children.esds) : {}
|
|
323
|
-
if (MP3_OTI.has(oti)) return
|
|
327
|
+
if (MP3_OTI.has(oti)) return mp3(boxes)
|
|
324
328
|
if (oti === 0xA5) return frames(import('@audio/decode-ac3'))
|
|
325
329
|
if (oti === 0xA9) return frames(import('@audio/decode-dts'))
|
|
326
330
|
if (oti === 0xA6) return frames(import('@audio/decode-eac3'))
|
|
327
331
|
if (!oti || AAC_OTI.has(oti)) {
|
|
328
332
|
if (!dsi) throw Error('MP4 AAC track has no AudioSpecificConfig')
|
|
329
|
-
return aac({ asc: dsi })
|
|
333
|
+
return aac({ asc: dsi }, boxes)
|
|
330
334
|
}
|
|
331
335
|
throw unsupported(UNSUPPORTED[oti] || 'esds object type 0x' + oti.toString(16))
|
|
332
336
|
}
|
|
333
337
|
if (type === 'alac') {
|
|
334
338
|
if (!children.alac) throw Error('MP4 ALAC track has no magic cookie')
|
|
335
|
-
return aac({ alac: children.alac })
|
|
339
|
+
return aac({ alac: children.alac }, boxes)
|
|
336
340
|
}
|
|
337
|
-
if (type === '.mp3') return
|
|
341
|
+
if (type === '.mp3') return mp3(boxes)
|
|
338
342
|
if (type === 'fLaC') {
|
|
339
343
|
if (!children.dfLa) throw Error('MP4 FLAC track has no dfLa box')
|
|
340
344
|
return flac(children.dfLa.subarray(4))
|
|
341
345
|
}
|
|
342
346
|
if (type === 'Opus') {
|
|
343
347
|
if (!children.dOps) throw Error('MP4 Opus track has no dOps box')
|
|
344
|
-
return opus(children.dOps)
|
|
348
|
+
return opus(children.dOps, boxes)
|
|
345
349
|
}
|
|
346
350
|
if (type === 'samr' || type === 'sawb') return amr(type === 'sawb')
|
|
347
351
|
if (type === 'ac-3') return frames(import('@audio/decode-ac3'))
|
|
@@ -383,8 +387,9 @@ function pcmFormat(e, ch) {
|
|
|
383
387
|
|
|
384
388
|
// --- adapters: { feed(frames: Uint8Array[]), flush(), free() } ---
|
|
385
389
|
|
|
386
|
-
async function aac(opts) {
|
|
387
|
-
let
|
|
390
|
+
async function aac(opts, boxes) {
|
|
391
|
+
let { decoder, gapless } = await import('@audio/decode-aac')
|
|
392
|
+
let dec = await decoder({ ...opts, gapless: gapless?.({ ...boxes, ...opts }) })
|
|
388
393
|
return { feed: frames => dec.decode(frames), flush: () => dec.flush(), free: () => dec.free() }
|
|
389
394
|
}
|
|
390
395
|
|
|
@@ -394,6 +399,28 @@ async function frames(load) {
|
|
|
394
399
|
return { feed: frames => dec.decode(concat(frames)), flush: () => dec.flush?.() ?? EMPTY, free: () => dec.free() }
|
|
395
400
|
}
|
|
396
401
|
|
|
402
|
+
// MP3 samples carry no LAME tag, so mpg123 decodes from the media's first sample: the edit list's window drops the
|
|
403
|
+
// encoder and decoder delay (1105 for LAME, as ffmpeg writes it) and the padding. decode-aac reads the window.
|
|
404
|
+
async function mp3(boxes) {
|
|
405
|
+
let [codec, { gapless }] = await Promise.all([frames(import('@audio/decode-mp3')), import('@audio/decode-aac')])
|
|
406
|
+
return windowed(codec, gapless?.(boxes))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// the edit list's window { start, duration } (seconds) over a codec that decodes from the media's first sample
|
|
410
|
+
function windowed(codec, g) {
|
|
411
|
+
if (!g) return codec
|
|
412
|
+
let skip = null, left = null
|
|
413
|
+
let cut = r => {
|
|
414
|
+
let n = r.channelData?.[0]?.length
|
|
415
|
+
if (!n) return r
|
|
416
|
+
skip ??= Math.round(g.start * r.sampleRate); left ??= g.duration == null ? Infinity : Math.round(g.duration * r.sampleRate)
|
|
417
|
+
let a = Math.min(n, skip), b = Math.min(n, a + left)
|
|
418
|
+
skip -= a; left -= b - a
|
|
419
|
+
return b <= a ? EMPTY : a || b < n ? { ...r, channelData: r.channelData.map(c => c.subarray(a, b)) } : r
|
|
420
|
+
}
|
|
421
|
+
return { feed: frames => cut(codec.feed(frames)), flush: () => cut(codec.flush()), free: codec.free }
|
|
422
|
+
}
|
|
423
|
+
|
|
397
424
|
async function flac(blocks) {
|
|
398
425
|
let dec = await (await import('@audio/decode-flac')).decoder()
|
|
399
426
|
let head = concat([new Uint8Array([0x66, 0x4C, 0x61, 0x43]), blocks]) // "fLaC" + METADATA_BLOCKs
|
|
@@ -405,16 +432,24 @@ async function flac(blocks) {
|
|
|
405
432
|
}
|
|
406
433
|
}
|
|
407
434
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
435
|
+
// Mapping family 1 orders 3–8 channels as Vorbis does (RFC 7845 §5.1.1.2): out in SMPTE/WAV order (L R C LFE Ls Rs …)
|
|
436
|
+
// as decode-opus reorders Ogg Opus: output channel j takes the coded channel of Vorbis position OPUS_ORDER[n][j]
|
|
437
|
+
const OPUS_ORDER = { 3: [0, 2, 1], 5: [0, 2, 1, 3, 4], 6: [0, 2, 1, 5, 3, 4], 7: [0, 2, 1, 6, 5, 3, 4], 8: [0, 2, 1, 7, 5, 6, 3, 4] }
|
|
438
|
+
|
|
439
|
+
// Opus in ISOBMFF: the edit list's media_time covers PreSkip and its duration is the real length, so with an edit
|
|
440
|
+
// list the window trims both ends from the media's first sample; without one, dOps' PreSkip trims the start
|
|
441
|
+
async function opus(dOps, boxes) {
|
|
442
|
+
let [{ createOpusDecoder }, { gapless }] = await Promise.all([import('@audio/decode-opus/core'), import('@audio/decode-aac')])
|
|
443
|
+
let g = gapless?.(boxes), channels = dOps[1]
|
|
444
|
+
let opts = { channels, sampleRate: 48000, preSkip: g ? 0 : r16(dOps, 2), outputGain: (r16(dOps, 8) << 16) >> 16 }
|
|
411
445
|
if (dOps[10] > 0) {
|
|
412
446
|
opts.streamCount = dOps[11]; opts.coupledStreamCount = dOps[12]
|
|
413
|
-
|
|
447
|
+
let table = Array.from(dOps.subarray(13, 13 + channels))
|
|
448
|
+
opts.channelMappingTable = dOps[10] === 1 && OPUS_ORDER[channels] ? OPUS_ORDER[channels].map(i => table[i]) : table
|
|
414
449
|
}
|
|
415
|
-
let dec = await
|
|
450
|
+
let dec = await createOpusDecoder()
|
|
416
451
|
dec.configure(opts)
|
|
417
|
-
return {
|
|
452
|
+
return windowed({
|
|
418
453
|
feed: frames => {
|
|
419
454
|
let r = dec.decodeFrames(frames)
|
|
420
455
|
if (!r.samplesDecoded) return EMPTY
|
|
@@ -422,7 +457,7 @@ async function opus(dOps) {
|
|
|
422
457
|
},
|
|
423
458
|
flush: () => EMPTY,
|
|
424
459
|
free: () => dec.free()
|
|
425
|
-
}
|
|
460
|
+
}, g)
|
|
426
461
|
}
|
|
427
462
|
|
|
428
463
|
async function amr(wb) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audio/decode-mp4",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Decode the audio track of MP4, MOV, M4A, M4V and 3GP files (AAC, ALAC, MP3, FLAC, Opus, AMR, PCM) to PCM samples",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "decode-mp4.js",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"LICENSE"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@audio/decode-aac": "^1.
|
|
30
|
+
"@audio/decode-aac": "^1.6.0",
|
|
31
31
|
"@audio/decode-ac3": "^1.0.0",
|
|
32
32
|
"@audio/decode-amr": "^1.3.0",
|
|
33
33
|
"@audio/decode-dts": "^1.0.0",
|
|
34
34
|
"@audio/decode-flac": "^1.3.0",
|
|
35
35
|
"@audio/decode-mp3": "^1.3.0",
|
|
36
|
-
"@audio/decode-opus": "^1.3.
|
|
36
|
+
"@audio/decode-opus": "^1.3.3",
|
|
37
37
|
"@audio/decode-eac3": "^1.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|