@audio/decode-aac 1.1.2 → 1.1.3

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-aac.js +78 -157
  2. package/package.json +1 -1
package/decode-aac.js CHANGED
@@ -61,7 +61,9 @@ class AACDecoder {
61
61
  this._ptr = 0
62
62
  this._cap = 0
63
63
  this._left = null
64
- this._m4a = null // { sizes: number[], idx: number } — M4A streaming state
64
+ this._fileOff = 0 // absolute file offset of _left[0] (M4A streaming)
65
+ this._skip = 0 // bytes to discard from incoming data (M4A streaming, when next frame is past _left)
66
+ this._m4a = null // M4A streaming iterator: { sizes, stco, stsc, idx, ci, sInC, spc, nextOff }
65
67
  this._accum = null // Uint8Array[] — M4A header accumulator
66
68
  this._accumLen = 0
67
69
  }
@@ -72,36 +74,18 @@ class AACDecoder {
72
74
 
73
75
  let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
74
76
 
75
- // M4A streming phase 2: extract frames by known sizes
76
77
  if (this._m4a) return this._feedM4AData(buf)
77
-
78
- // M4A accumulating: waiting for moov + mdat header
79
- if (this._accum) {
80
- this._accum.push(buf)
81
- this._accumLen += buf.length
82
- return this._tryM4AInit()
83
- }
84
-
85
- // ADTS mode (already initialized)
86
78
  if (this.h) return this._decodeADTS(buf)
87
-
88
- // First call: detect format
89
- if (buf.length > 8 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) {
90
- this._accum = [buf]
91
- this._accumLen = buf.length
79
+ // M4A: accumulating moov+mdat header, or first chunk starts with ftyp
80
+ if (this._accum || (buf.length > 8 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70)) {
81
+ (this._accum ??= []).push(buf)
82
+ this._accumLen += buf.length
92
83
  return this._tryM4AInit()
93
84
  }
94
-
95
85
  return this._decodeADTS(buf)
96
86
  }
97
87
 
98
88
  flush() {
99
- if (this._accum?.length) {
100
- // M4A moov-last: one-shot decode on accumulated data
101
- let buf = this._catAccum()
102
- this._accum = null; this._accumLen = 0
103
- return this._decodeM4A(buf)
104
- }
105
89
  this._left = null
106
90
  return EMPTY
107
91
  }
@@ -121,6 +105,7 @@ class AACDecoder {
121
105
  }
122
106
  this._accum = null; this._accumLen = 0
123
107
  this._m4a = null; this._left = null
108
+ this._fileOff = 0; this._skip = 0
124
109
  }
125
110
 
126
111
  _catAccum() {
@@ -133,18 +118,16 @@ class AACDecoder {
133
118
  _tryM4AInit() {
134
119
  let buf = this._catAccum()
135
120
  let asc = null, stsz = null, stco = null, stsc = null
136
- let mdatOff = 0, mdatLen = 0
137
121
 
138
- parseBoxes(buf, 0, buf.length, (type, data, off) => {
122
+ parseBoxes(buf, 0, buf.length, (type, data) => {
139
123
  if (type === 'esds') asc = parseEsds(data)
140
124
  else if (type === 'stsz') stsz = parseStsz(data)
141
125
  else if (type === 'stco') stco = parseStco(data)
142
126
  else if (type === 'co64') stco = parseCo64(data)
143
127
  else if (type === 'stsc') stsc = parseStsc(data)
144
- else if (type === 'mdat') { mdatOff = off; mdatLen = data.length }
145
128
  })
146
129
 
147
- if (!asc) return EMPTY // moov not found yet
130
+ if (!asc || !stsz || !stco?.length) return EMPTY // moov/tables not ready
148
131
 
149
132
  // Init WASM decoder with ASC
150
133
  let m = this.m, h = m._aac_create()
@@ -158,47 +141,52 @@ class AACDecoder {
158
141
  if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
159
142
  this.h = h
160
143
 
161
- // Extract available frames using stco (correct absolute offsets)
162
- let frames = (stsz && stco)
163
- ? extractFrames(buf, stsz, stco, stsc)
164
- : mdatLen ? scanMdat(buf, mdatOff, mdatLen) : []
165
-
166
- let decodedIdx = frames.length
167
- this._m4a = { sizes: stsz || [], idx: decodedIdx }
144
+ // Streaming: walk sample tables by absolute file offset so chunk boundaries are irrelevant.
168
145
  this._accum = null; this._accumLen = 0
169
-
170
- // For streaming: compute leftover mdat data after extracted frames
171
- if (stsz && decodedIdx < stsz.length && decodedIdx > 0 && stco?.length) {
172
- let consumed = 0
173
- for (let i = 0; i < decodedIdx; i++) consumed += stsz[i]
174
- let dataStart = stco[0] + consumed
175
- if (dataStart < buf.length) this._left = buf.subarray(dataStart).slice()
176
- }
177
-
178
- if (!frames.length) return EMPTY
179
- return this._feedFrames(frames)
146
+ this._m4a = { sizes: stsz, stco, stsc, idx: 0, ci: 0, sInC: 0, spc: spcAt(0, stsc), nextOff: stco[0] }
147
+ this._left = buf
148
+ this._fileOff = 0
149
+ this._skip = 0
150
+ return this._extractM4A()
180
151
  }
181
152
 
182
153
  _feedM4AData(buf) {
183
- let st = this._m4a
184
- if (this._left) {
185
- let merged = new Uint8Array(this._left.length + buf.length)
186
- merged.set(this._left); merged.set(buf, this._left.length)
187
- buf = merged; this._left = null
154
+ if (this._skip > 0) {
155
+ let n = Math.min(this._skip, buf.length)
156
+ this._skip -= n
157
+ this._fileOff += n
158
+ buf = buf.subarray(n)
159
+ if (!buf.length) return EMPTY
188
160
  }
161
+ this._left = append(this._left, buf)
162
+ return this._extractM4A()
163
+ }
189
164
 
190
- let frames = [], pos = 0
165
+ _extractM4A() {
166
+ let st = this._m4a, frames = []
191
167
  while (st.idx < st.sizes.length) {
192
- let sz = st.sizes[st.idx]
193
- if (pos + sz > buf.length) break
194
- frames.push(buf.subarray(pos, pos + sz))
195
- pos += sz
196
- st.idx++
168
+ let off = st.nextOff, sz = st.sizes[st.idx]
169
+ let bufOff = off - this._fileOff
170
+ if (bufOff + sz > this._left.length) break
171
+ if (bufOff >= 0) frames.push(this._left.subarray(bufOff, bufOff + sz))
172
+ advanceM4A(st)
197
173
  }
198
174
 
199
- if (pos < buf.length) this._left = buf.subarray(pos).slice()
200
- if (!frames.length) return EMPTY
201
- return this._feedFrames(frames)
175
+ if (st.idx < st.sizes.length) {
176
+ let nextOff = st.nextOff, end = this._fileOff + this._left.length
177
+ if (nextOff >= end) {
178
+ this._skip = nextOff - end
179
+ this._fileOff = end
180
+ this._left = null
181
+ } else if (nextOff > this._fileOff) {
182
+ this._left = this._left.subarray(nextOff - this._fileOff).slice()
183
+ this._fileOff = nextOff
184
+ }
185
+ } else {
186
+ this._left = null
187
+ }
188
+
189
+ return frames.length ? this._feedFrames(frames) : EMPTY
202
190
  }
203
191
 
204
192
  _alloc(len) {
@@ -213,14 +201,7 @@ class AACDecoder {
213
201
  _decodeADTS(buf) {
214
202
  let m = this.m
215
203
 
216
- // prepend leftover from previous call
217
- if (this._left) {
218
- let merged = new Uint8Array(this._left.length + buf.length)
219
- merged.set(this._left)
220
- merged.set(buf, this._left.length)
221
- buf = merged
222
- this._left = null
223
- }
204
+ if (this._left) { buf = append(this._left, buf); this._left = null }
224
205
 
225
206
  if (!this.h) {
226
207
  if (buf.length < 7) { this._left = buf.slice(); return EMPTY }
@@ -261,27 +242,6 @@ class AACDecoder {
261
242
  return this._feedFrames(frames)
262
243
  }
263
244
 
264
- _decodeM4A(buf) {
265
- let { asc, frames } = demuxM4A(buf)
266
- if (!asc || !frames.length) return EMPTY
267
-
268
- let m = this.m
269
- let h = m._aac_create()
270
-
271
- let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
272
- let ptr = this._alloc(asc.length)
273
- m.HEAPU8.set(asc, ptr)
274
- let err = m._aac_init2(h, ptr, asc.length, srP, chP)
275
- if (err < 0) { m._aac_close(h); throw Error('M4A init failed (code ' + err + ')') }
276
-
277
- this.sr = m.getValue(srP, 'i32')
278
- this.ch = m.getValue(chP, 'i8')
279
- if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
280
- this.h = h
281
-
282
- return this._feedFrames(frames)
283
- }
284
-
285
245
  _feedFrames(frames) {
286
246
  let m = this.m, h = this.h
287
247
  let chunks = [], totalPerCh = 0, channels = this.ch, errors = 0
@@ -322,26 +282,11 @@ class AACDecoder {
322
282
 
323
283
  // ===== M4A demuxer =====
324
284
 
325
- function demuxM4A(buf) {
326
- let asc = null, stsz = null, stco = null, stsc = null
327
- let mdatOff = 0, mdatLen = 0
328
-
329
- parseBoxes(buf, 0, buf.length, (type, data, off) => {
330
- if (type === 'esds') asc = parseEsds(data)
331
- else if (type === 'stsz') stsz = parseStsz(data)
332
- else if (type === 'stco') stco = parseStco(data)
333
- else if (type === 'co64') stco = parseCo64(data)
334
- else if (type === 'stsc') stsc = parseStsc(data)
335
- else if (type === 'mdat') { mdatOff = off; mdatLen = data.length }
336
- })
337
-
338
- if (!asc) return { asc: null, frames: [] }
339
-
340
- let frames = (stsz && stco)
341
- ? extractFrames(buf, stsz, stco, stsc)
342
- : mdatLen ? scanMdat(buf, mdatOff, mdatLen) : []
343
-
344
- return { asc, frames }
285
+ function append(left, buf) {
286
+ if (!left?.length) return buf.slice()
287
+ let merged = new Uint8Array(left.length + buf.length)
288
+ merged.set(left); merged.set(buf, left.length)
289
+ return merged
345
290
  }
346
291
 
347
292
  const CONTAINERS = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl', 'udta', 'meta', 'edts', 'sinf'])
@@ -352,28 +297,21 @@ function parseBoxes(buf, start, end, cb) {
352
297
  let size = r32(buf, off)
353
298
  let type = String.fromCharCode(buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7])
354
299
 
355
- if (size === 0) {
356
- size = end - off
357
- } else if (size === 1 && off + 16 <= end) {
300
+ if (size === 0) size = end - off
301
+ else if (size === 1 && off + 16 <= end) {
358
302
  size = r32(buf, off + 12)
359
303
  if (size < 16) break
360
- } else if (size < 8) {
361
- break
362
- }
304
+ } else if (size < 8) break
363
305
 
364
- let bodyOff = off + 8
365
- let truncated = off + size > end
306
+ // skip mdat fast — the raw frames don't interest us here
307
+ if (type === 'mdat') { off += size; continue }
308
+ // truncated non-mdat box: tables would be garbage — wait for more data
309
+ if (off + size > end) break
366
310
 
367
- // mdat carries raw frames — partial is fine; callers honour availability
368
- if (type === 'mdat') {
369
- cb(type, buf.subarray(bodyOff, truncated ? end : off + size), bodyOff)
370
- if (truncated) break
371
- }
372
- // any other box must be fully present before parsing — partial tables yield garbage
373
- else if (truncated) break
374
- else if (type === 'stsd') parseSampleDesc(buf, bodyOff, size - 8, cb)
311
+ let bodyOff = off + 8
312
+ if (type === 'stsd') parseSampleDesc(buf, bodyOff, size - 8, cb)
375
313
  else if (CONTAINERS.has(type)) parseBoxes(buf, bodyOff + (type === 'meta' ? 4 : 0), off + size, cb)
376
- else cb(type, buf.subarray(bodyOff, off + size), bodyOff)
314
+ else cb(type, buf.subarray(bodyOff, off + size))
377
315
 
378
316
  off += size
379
317
  }
@@ -428,41 +366,24 @@ function parseStsc(data) {
428
366
  return e
429
367
  }
430
368
 
431
- function extractFrames(buf, stsz, stco, stsc) {
432
- let frames = [], si = 0
433
- for (let ci = 0; ci < stco.length; ci++) {
434
- let spc = 1
435
- if (stsc?.length) {
436
- let cn = ci + 1
437
- for (let j = stsc.length - 1; j >= 0; j--)
438
- if (cn >= stsc[j].first) { spc = stsc[j].spc; break }
439
- }
440
- let off = stco[ci]
441
- for (let s = 0; s < spc && si < stsz.length; s++) {
442
- let sz = stsz[si]
443
- if (off + sz > buf.length) return frames // sequential: first missing → rest missing
444
- frames.push(buf.subarray(off, off + sz))
445
- si++
446
- off += sz
447
- }
448
- }
449
- return frames
369
+ function spcAt(ci, stsc) {
370
+ if (!stsc?.length) return 1
371
+ let spc = 1, cn = ci + 1
372
+ for (let j = stsc.length - 1; j >= 0; j--)
373
+ if (cn >= stsc[j].first) { spc = stsc[j].spc; break }
374
+ return spc
450
375
  }
451
376
 
452
- function scanMdat(buf, off, len) {
453
- let frames = [], end = off + len, pos = off
454
- while (pos < end - 7) {
455
- if (buf[pos] === 0xFF && (buf[pos + 1] & 0xF6) === 0xF0) {
456
- let flen = ((buf[pos + 3] & 0x03) << 11) | (buf[pos + 4] << 3) | (buf[pos + 5] >> 5)
457
- if (flen > 0 && pos + flen <= end) {
458
- frames.push(buf.subarray(pos, pos + flen))
459
- pos += flen
460
- continue
461
- }
462
- }
463
- pos++
377
+ function advanceM4A(st) {
378
+ st.nextOff += st.sizes[st.idx]
379
+ st.idx++
380
+ st.sInC++
381
+ if (st.sInC >= st.spc && st.ci + 1 < st.stco.length) {
382
+ st.ci++
383
+ st.sInC = 0
384
+ st.spc = spcAt(st.ci, st.stsc)
385
+ st.nextOff = st.stco[st.ci]
464
386
  }
465
- return frames
466
387
  }
467
388
 
468
389
  function r32(buf, off) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audio/decode-aac",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Decode AAC/M4A audio via FAAD2 WASM",
5
5
  "type": "module",
6
6
  "main": "decode-aac.js",