@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.
Files changed (2) hide show
  1. package/decode-mp4.js +52 -17
  2. 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 frames(import('@audio/decode-mp3'))
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 frames(import('@audio/decode-mp3'))
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 dec = await (await import('@audio/decode-aac')).decoder(opts)
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
- async function opus(dOps) {
409
- let channels = dOps[1]
410
- let opts = { channels, sampleRate: 48000, preSkip: r16(dOps, 2), outputGain: (r16(dOps, 8) << 16) >> 16 }
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
- opts.channelMappingTable = Array.from(dOps.subarray(13, 13 + channels))
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 (await import('@audio/decode-opus/core')).createOpusDecoder()
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.2.0",
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.5.0",
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.0",
36
+ "@audio/decode-opus": "^1.3.3",
37
37
  "@audio/decode-eac3": "^1.0.0"
38
38
  },
39
39
  "devDependencies": {