@audio/decode-mp4 1.2.0 → 1.3.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.
Files changed (2) hide show
  1. package/decode-mp4.js +40 -10
  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,25 +318,27 @@ 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 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))
@@ -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,26 @@ 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
+ let g = gapless?.(boxes)
407
+ if (!g) return codec
408
+ let skip = null, left = null
409
+ return {
410
+ feed(frames) {
411
+ let r = codec.feed(frames), n = r.channelData?.[0]?.length
412
+ if (!n) return r
413
+ skip ??= Math.round(g.start * r.sampleRate); left ??= g.duration == null ? Infinity : Math.round(g.duration * r.sampleRate)
414
+ let a = Math.min(n, skip), b = Math.min(n, a + left)
415
+ skip -= a; left -= b - a
416
+ return b <= a ? EMPTY : a || b < n ? { ...r, channelData: r.channelData.map(c => c.subarray(a, b)) } : r
417
+ },
418
+ flush: codec.flush, free: codec.free
419
+ }
420
+ }
421
+
397
422
  async function flac(blocks) {
398
423
  let dec = await (await import('@audio/decode-flac')).decoder()
399
424
  let head = concat([new Uint8Array([0x66, 0x4C, 0x61, 0x43]), blocks]) // "fLaC" + METADATA_BLOCKs
@@ -405,12 +430,17 @@ async function flac(blocks) {
405
430
  }
406
431
  }
407
432
 
433
+ // 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 …)
434
+ // as decode-opus reorders Ogg Opus: output channel j takes the coded channel of Vorbis position OPUS_ORDER[n][j]
435
+ 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] }
436
+
408
437
  async function opus(dOps) {
409
438
  let channels = dOps[1]
410
439
  let opts = { channels, sampleRate: 48000, preSkip: r16(dOps, 2), outputGain: (r16(dOps, 8) << 16) >> 16 }
411
440
  if (dOps[10] > 0) {
412
441
  opts.streamCount = dOps[11]; opts.coupledStreamCount = dOps[12]
413
- opts.channelMappingTable = Array.from(dOps.subarray(13, 13 + channels))
442
+ let table = Array.from(dOps.subarray(13, 13 + channels))
443
+ opts.channelMappingTable = dOps[10] === 1 && OPUS_ORDER[channels] ? OPUS_ORDER[channels].map(i => table[i]) : table
414
444
  }
415
445
  let dec = await (await import('@audio/decode-opus/core')).createOpusDecoder()
416
446
  dec.configure(opts)
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.0",
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": {