Skip to content

Commit b9dd3bf

Browse files
committed
Fix #45
1 parent ba982a2 commit b9dd3bf

4 files changed

Lines changed: 137 additions & 159 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "audio-decode",
3-
"version": "3.9.1",
3+
"version": "3.9.2",
44
"description": "Decode audio data in node or browser",
55
"type": "module",
66
"sideEffects": false,

packages/decode-aac/decode-aac.js

Lines changed: 78 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ class AACDecoder {
6161
this._ptr = 0
6262
this._cap = 0
6363
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 }
6567
this._accum = null // Uint8Array[] — M4A header accumulator
6668
this._accumLen = 0
6769
}
@@ -72,36 +74,18 @@ class AACDecoder {
7274

7375
let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
7476

75-
// M4A streming phase 2: extract frames by known sizes
7677
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)
8678
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
9283
return this._tryM4AInit()
9384
}
94-
9585
return this._decodeADTS(buf)
9686
}
9787

9888
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-
}
10589
this._left = null
10690
return EMPTY
10791
}
@@ -121,6 +105,7 @@ class AACDecoder {
121105
}
122106
this._accum = null; this._accumLen = 0
123107
this._m4a = null; this._left = null
108+
this._fileOff = 0; this._skip = 0
124109
}
125110

126111
_catAccum() {
@@ -133,18 +118,16 @@ class AACDecoder {
133118
_tryM4AInit() {
134119
let buf = this._catAccum()
135120
let asc = null, stsz = null, stco = null, stsc = null
136-
let mdatOff = 0, mdatLen = 0
137121

138-
parseBoxes(buf, 0, buf.length, (type, data, off) => {
122+
parseBoxes(buf, 0, buf.length, (type, data) => {
139123
if (type === 'esds') asc = parseEsds(data)
140124
else if (type === 'stsz') stsz = parseStsz(data)
141125
else if (type === 'stco') stco = parseStco(data)
142126
else if (type === 'co64') stco = parseCo64(data)
143127
else if (type === 'stsc') stsc = parseStsc(data)
144-
else if (type === 'mdat') { mdatOff = off; mdatLen = data.length }
145128
})
146129

147-
if (!asc) return EMPTY // moov not found yet
130+
if (!asc || !stsz || !stco?.length) return EMPTY // moov/tables not ready
148131

149132
// Init WASM decoder with ASC
150133
let m = this.m, h = m._aac_create()
@@ -158,47 +141,52 @@ class AACDecoder {
158141
if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
159142
this.h = h
160143

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.
168145
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()
180151
}
181152

182153
_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
188160
}
161+
this._left = append(this._left, buf)
162+
return this._extractM4A()
163+
}
189164

190-
let frames = [], pos = 0
165+
_extractM4A() {
166+
let st = this._m4a, frames = []
191167
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)
197173
}
198174

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
202190
}
203191

204192
_alloc(len) {
@@ -213,14 +201,7 @@ class AACDecoder {
213201
_decodeADTS(buf) {
214202
let m = this.m
215203

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 }
224205

225206
if (!this.h) {
226207
if (buf.length < 7) { this._left = buf.slice(); return EMPTY }
@@ -261,27 +242,6 @@ class AACDecoder {
261242
return this._feedFrames(frames)
262243
}
263244

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-
285245
_feedFrames(frames) {
286246
let m = this.m, h = this.h
287247
let chunks = [], totalPerCh = 0, channels = this.ch, errors = 0
@@ -322,26 +282,11 @@ class AACDecoder {
322282

323283
// ===== M4A demuxer =====
324284

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
345290
}
346291

347292
const CONTAINERS = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl', 'udta', 'meta', 'edts', 'sinf'])
@@ -352,28 +297,21 @@ function parseBoxes(buf, start, end, cb) {
352297
let size = r32(buf, off)
353298
let type = String.fromCharCode(buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7])
354299

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) {
358302
size = r32(buf, off + 12)
359303
if (size < 16) break
360-
} else if (size < 8) {
361-
break
362-
}
304+
} else if (size < 8) break
363305

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
366310

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)
375313
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))
377315

378316
off += size
379317
}
@@ -428,41 +366,24 @@ function parseStsc(data) {
428366
return e
429367
}
430368

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
450375
}
451376

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]
464386
}
465-
return frames
466387
}
467388

468389
function r32(buf, off) {

packages/decode-aac/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@audio/decode-aac",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "Decode AAC/M4A audio via FAAD2 WASM",
55
"type": "module",
66
"main": "decode-aac.js",

0 commit comments

Comments
 (0)