@audio/decode-aac 1.1.2 → 1.2.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 (5) hide show
  1. package/README.md +13 -4
  2. package/alac.js +326 -0
  3. package/decode-aac.js +125 -172
  4. package/meta.js +81 -0
  5. package/package.json +9 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @audio/decode-aac
2
2
 
3
- Decode AAC/M4A audio to PCM float samples. FAAD2 compiled to WASM — works in Node.js and browsers, no native dependencies.
3
+ Decode AAC/M4A and ALAC (Apple Lossless) audio to PCM float samples. AAC via FAAD2 (WASM); ALAC via a pure-JS port of Apple's reference decoder — auto-detected from the M4A sample entry. Works in Node.js and browsers, no native dependencies.
4
4
 
5
5
  ## Install
6
6
 
@@ -51,10 +51,19 @@ Creates a decoder instance for manual control.
51
51
 
52
52
  ## Formats
53
53
 
54
- - M4A / MP4 with AAC audio
54
+ - M4A / MP4 with AAC audio (LC, HE-AAC v1/v2 — SBR, PS)
55
+ - M4A / MP4 with ALAC (Apple Lossless), 16/20/24/32-bit — pure JS, bit-exact
55
56
  - Raw ADTS streams (.aac)
56
- - LC, HE-AAC v1/v2 (SBR, PS)
57
+
58
+ ## Metadata
59
+
60
+ ```js
61
+ import { parseMeta } from '@audio/decode-aac/meta'
62
+
63
+ let { meta, sampleRate } = parseMeta(m4aBytes)
64
+ // meta: { title, artist, album, year, genre, track, ..., pictures }
65
+ ```
57
66
 
58
67
  ## License
59
68
 
60
- GPL-2.0 (FAAD2) — [krishnized](https://github.com/krishnized/license)
69
+ AAC decoding: GPL-2.0 (FAAD2). ALAC decoding: Apache-2.0 (port of Apple's ALAC reference). — [krishnized](https://github.com/krishnized/license)
package/alac.js ADDED
@@ -0,0 +1,326 @@
1
+ /**
2
+ * ALAC (Apple Lossless) decoder — pure JS.
3
+ * Ported from Apple's ALAC reference decoder (Apache License 2.0):
4
+ * ALACDecoder.cpp, ag_dec.c, dp_dec.c, matrix_dec.c, ALACBitUtilities.c
5
+ * Copyright (c) 2011 Apple Inc. https://github.com/macosforge/alac
6
+ *
7
+ * createALAC(cookie) → { config, decodeFrame(frameBytes) → { channelData: Float32Array[], numSamples } }
8
+ */
9
+
10
+ // ── constants (aglib.h) ──────────────────────────────────────────────
11
+ const QBSHIFT = 9, QB = 1 << QBSHIFT
12
+ const MMULSHIFT = 2, MDENSHIFT = QBSHIFT - MMULSHIFT - 1, MOFF = 1 << (MDENSHIFT - 2)
13
+ const BITOFF = 24
14
+ const MAX_PREFIX_16 = 9, MAX_PREFIX_32 = 9, MAX_DATATYPE_BITS_16 = 16
15
+ const N_MAX_MEAN_CLAMP = 0xffff, N_MEAN_CLAMP_VAL = 0xffff
16
+ // MPEG element ids (ALACBitUtilities.h)
17
+ const ID_SCE = 0, ID_CPE = 1, ID_LFE = 3, ID_DSE = 4, ID_FIL = 6, ID_END = 7
18
+
19
+ // ── bit reader (ALACBitUtilities.c) ──────────────────────────────────
20
+ // pos = absolute bit index into data; data is padded with 8 trailing zeros so
21
+ // 32-bit window reads near the end are always in-bounds.
22
+ function bits(data, realLen) { return { data, pos: 0, realLen } }
23
+ function read(bb, n) { // BitBufferRead — up to 16 bits, MSB first
24
+ let p = bb.pos, o = p >> 3, d = bb.data
25
+ let v = (((d[o] << 16) | (d[o + 1] << 8) | d[o + 2]) << (p & 7)) & 0xFFFFFF
26
+ bb.pos = p + n
27
+ return v >>> (24 - n)
28
+ }
29
+ function readOne(bb) { let p = bb.pos; bb.pos = p + 1; return (bb.data[p >> 3] >> (7 - (p & 7))) & 1 }
30
+ function byteAlign(bb) { bb.pos = (bb.pos + 7) & ~7 }
31
+ function read32(d, o) { return (((d[o] << 24) | (d[o + 1] << 16) | (d[o + 2] << 8) | d[o + 3]) >>> 0) }
32
+
33
+ function getstreambits(d, bitoffset, numbits) {
34
+ let byteoffset = bitoffset >>> 3
35
+ let load1 = read32(d, byteoffset), result
36
+ if ((numbits + (bitoffset & 7)) > 32) {
37
+ result = (load1 << (bitoffset & 7)) >>> 0
38
+ let load2 = d[byteoffset + 4]
39
+ load2 = load2 >>> (8 - (numbits + (bitoffset & 7) - 32))
40
+ result = result >>> (32 - numbits)
41
+ result = (result | load2) >>> 0
42
+ } else {
43
+ result = load1 >>> (32 - numbits - (bitoffset & 7))
44
+ }
45
+ if (numbits !== 32) result = (result & (((1 << numbits) >>> 0) - 1)) >>> 0
46
+ return result >>> 0
47
+ }
48
+
49
+ // ── adaptive Golomb decode (ag_dec.c) ────────────────────────────────
50
+ function lg3a(x) { return 31 - Math.clz32(x + 3) }
51
+
52
+ // st = { p } carries the local bit position
53
+ function dynGet32(d, st, m, k, maxbits) {
54
+ let tempbits = st.p
55
+ let streamlong = (read32(d, tempbits >> 3) << (tempbits & 7)) >>> 0
56
+ let result = Math.clz32((~streamlong) >>> 0)
57
+ if (result >= MAX_PREFIX_32) {
58
+ result = getstreambits(d, tempbits + MAX_PREFIX_32, maxbits)
59
+ tempbits += MAX_PREFIX_32 + maxbits
60
+ } else {
61
+ tempbits += result + 1
62
+ if (k !== 1) {
63
+ streamlong = (streamlong << (result + 1)) >>> 0
64
+ let v = streamlong >>> (32 - k)
65
+ tempbits += k - 1
66
+ result = Math.imul(result, m)
67
+ if (v >= 2) { result += v - 1; tempbits += 1 }
68
+ }
69
+ }
70
+ st.p = tempbits
71
+ return result
72
+ }
73
+
74
+ function dynGet(d, st, m, k) {
75
+ let tempbits = st.p
76
+ let streamlong = (read32(d, tempbits >> 3) << (tempbits & 7)) >>> 0
77
+ let pre = Math.clz32((~streamlong) >>> 0), result
78
+ if (pre >= MAX_PREFIX_16) {
79
+ pre = MAX_PREFIX_16
80
+ tempbits += pre
81
+ streamlong = (streamlong << pre) >>> 0
82
+ result = streamlong >>> (32 - MAX_DATATYPE_BITS_16)
83
+ tempbits += MAX_DATATYPE_BITS_16
84
+ } else {
85
+ tempbits += pre + 1
86
+ streamlong = (streamlong << (pre + 1)) >>> 0
87
+ let v = streamlong >>> (32 - k)
88
+ tempbits += k
89
+ result = Math.imul(pre, m) + v - 1
90
+ if (v < 2) { result -= (v - 1); tempbits -= 1 }
91
+ }
92
+ st.p = tempbits
93
+ return result
94
+ }
95
+
96
+ // fills pc[0..numSamples) with residuals; advances bb.pos
97
+ function dynDecomp(p, bb, pc, numSamples, maxSize) {
98
+ let d = bb.data, st = { p: bb.pos }, maxPos = bb.realLen * 8
99
+ let mb = p.mb0, pb = p.pb, kb = p.kb, wb = p.wb
100
+ let zmode = 0, c = 0, o = 0
101
+ while (c < numSamples) {
102
+ if (st.p >= maxPos) throw Error('ALAC: ran past frame')
103
+ let m = mb >>> QBSHIFT
104
+ let k = Math.min(lg3a(m), kb)
105
+ m = (1 << k) - 1
106
+ let n = dynGet32(d, st, m, k, maxSize)
107
+ let ndecode = n + zmode
108
+ let mult = (-(ndecode & 1)) | 1
109
+ pc[o++] = ((ndecode + 1) >> 1) * mult
110
+ c++
111
+ mb = pb * (n + zmode) + mb - ((pb * mb) >> QBSHIFT)
112
+ if (n > N_MAX_MEAN_CLAMP) mb = N_MEAN_CLAMP_VAL
113
+ zmode = 0
114
+ if (((mb << MMULSHIFT) < QB) && (c < numSamples)) {
115
+ zmode = 1
116
+ k = Math.clz32(mb) - BITOFF + ((mb + MOFF) >> MDENSHIFT)
117
+ let mz = ((1 << k) - 1) & wb
118
+ n = dynGet(d, st, mz, k)
119
+ if (c + n > numSamples) throw Error('ALAC: zero-run overflow')
120
+ for (let j = 0; j < n; j++) { pc[o++] = 0; c++ }
121
+ if (n >= 65535) zmode = 0
122
+ mb = 0
123
+ }
124
+ }
125
+ bb.pos = st.p
126
+ }
127
+
128
+ // ── dynamic predictor (dp_dec.c) ─────────────────────────────────────
129
+ function signOf(i) { return (((-i) >>> 31) | (i >> 31)) }
130
+
131
+ function unpcBlock(pc1, out, num, coefs, numactive, chanbits, denshift) {
132
+ let chanshift = 32 - chanbits, denhalf = 1 << (denshift - 1)
133
+ out[0] = pc1[0]
134
+ if (numactive === 0) { for (let i = 1; i < num; i++) out[i] = pc1[i]; return }
135
+ if (numactive === 31) {
136
+ let prev = out[0]
137
+ for (let j = 1; j < num; j++) { let del = pc1[j] + prev; prev = (del << chanshift) >> chanshift; out[j] = prev }
138
+ return
139
+ }
140
+ for (let j = 1; j <= numactive; j++) { let del = pc1[j] + out[j - 1]; out[j] = (del << chanshift) >> chanshift }
141
+ let lim = numactive + 1
142
+ for (let j = lim; j < num; j++) {
143
+ let sum1 = 0, pb = j - 1, top = out[j - lim]
144
+ for (let kk = 0; kk < numactive; kk++) sum1 = (sum1 + Math.imul(coefs[kk], out[pb - kk] - top)) | 0
145
+ let del = pc1[j], del0 = del, sg = signOf(del)
146
+ del = (del + top + ((sum1 + denhalf) >> denshift)) | 0
147
+ out[j] = (del << chanshift) >> chanshift
148
+ if (sg > 0) {
149
+ for (let kk = numactive - 1; kk >= 0; kk--) {
150
+ let dd = top - out[pb - kk], sgn = signOf(dd)
151
+ coefs[kk] -= sgn
152
+ del0 -= Math.imul(numactive - kk, (Math.imul(sgn, dd) >> denshift))
153
+ if (del0 <= 0) break
154
+ }
155
+ } else if (sg < 0) {
156
+ for (let kk = numactive - 1; kk >= 0; kk--) {
157
+ let dd = top - out[pb - kk], sgn = signOf(dd)
158
+ coefs[kk] += sgn
159
+ del0 -= Math.imul(numactive - kk, (Math.imul(-sgn, dd) >> denshift))
160
+ if (del0 >= 0) break
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ // ── magic cookie (ALACSpecificConfig) ────────────────────────────────
167
+ function parseConfig(c) {
168
+ let u32 = (o) => (c[o] * 0x1000000) + (c[o + 1] << 16) + (c[o + 2] << 8) + c[o + 3]
169
+ return {
170
+ frameLength: u32(0), bitDepth: c[5], pb: c[6], mb: c[7], kb: c[8],
171
+ numChannels: c[9], maxRun: (c[10] << 8) | c[11],
172
+ sampleRate: u32(20),
173
+ }
174
+ }
175
+
176
+ export function createALAC(cookie) {
177
+ let cfg = parseConfig(cookie)
178
+ let N = cfg.frameLength
179
+ let mixU = new Int32Array(N), mixV = new Int32Array(N), pred = new Int32Array(N)
180
+ let shiftBuf = new Int32Array(N * 2)
181
+ let scale = 1 / Math.pow(2, cfg.bitDepth - 1)
182
+
183
+ function agParams(pbFactor) {
184
+ let pb = ((cfg.pb * pbFactor) / 4) | 0
185
+ return { mb0: cfg.mb, pb, kb: cfg.kb, wb: (1 << cfg.kb) - 1 }
186
+ }
187
+
188
+ // decode one ALAC frame → per-channel Int32 samples; returns numSamples
189
+ function decodeElements(bb, chans, requested) {
190
+ let chanIndex = 0, numSamples = requested
191
+ while (true) {
192
+ if (bb.pos >> 3 >= bb.realLen) break
193
+ let tag = read(bb, 3)
194
+ if (tag === ID_SCE || tag === ID_LFE) {
195
+ read(bb, 4) // element instance tag
196
+ if (read(bb, 12) !== 0) throw Error('ALAC: bad header')
197
+ let hb = read(bb, 4)
198
+ let partial = hb >> 3, bytesShifted = (hb >> 1) & 3, escape = hb & 1
199
+ if (bytesShifted === 3) throw Error('ALAC: bad shift')
200
+ let shift = bytesShifted * 8
201
+ let chanBits = cfg.bitDepth - shift
202
+ if (partial) numSamples = ((read(bb, 16) << 16) | read(bb, 16)) >>> 0
203
+ if (!escape) {
204
+ read(bb, 8); read(bb, 8) // mixBits, mixRes (unused for mono)
205
+ let hu = read(bb, 8), modeU = hu >> 4, denShiftU = hu & 0xf
206
+ let pu = read(bb, 8), pbFactorU = pu >> 5, numU = pu & 0x1f
207
+ let coefsU = new Int16Array(32)
208
+ for (let i = 0; i < numU; i++) coefsU[i] = read(bb, 16)
209
+ let shiftPos = bb.pos
210
+ if (bytesShifted) bb.pos += shift * numSamples
211
+ dynDecomp(agParams(pbFactorU), bb, pred, numSamples, chanBits)
212
+ if (modeU === 0) unpcBlock(pred, mixU, numSamples, coefsU, numU, chanBits, denShiftU)
213
+ else { unpcBlock(pred, pred, numSamples, null, 31, chanBits, 0); unpcBlock(pred, mixU, numSamples, coefsU, numU, chanBits, denShiftU) }
214
+ readShift(bb, shiftPos, shift, numSamples, 1)
215
+ } else {
216
+ let s = 32 - chanBits
217
+ if (chanBits <= 16) {
218
+ for (let i = 0; i < numSamples; i++) mixU[i] = (read(bb, chanBits) << s) >> s
219
+ } else {
220
+ let extra = chanBits - 16
221
+ for (let i = 0; i < numSamples; i++) { let v = (read(bb, 16) << 16) >> s; mixU[i] = v | read(bb, extra) }
222
+ }
223
+ bytesShifted = 0
224
+ }
225
+ let out = new Int32Array(numSamples)
226
+ for (let i = 0; i < numSamples; i++) out[i] = bytesShifted ? ((mixU[i] << shift) | shiftBuf[i]) : mixU[i]
227
+ chans[chanIndex++] = out
228
+ } else if (tag === ID_CPE) {
229
+ read(bb, 4)
230
+ if (read(bb, 12) !== 0) throw Error('ALAC: bad header')
231
+ let hb = read(bb, 4)
232
+ let partial = hb >> 3, bytesShifted = (hb >> 1) & 3, escape = hb & 1
233
+ if (bytesShifted === 3) throw Error('ALAC: bad shift')
234
+ let shift = bytesShifted * 8
235
+ let chanBits = cfg.bitDepth - shift + 1
236
+ if (partial) numSamples = ((read(bb, 16) << 16) | read(bb, 16)) >>> 0
237
+ let mixBits = 0, mixRes = 0
238
+ if (!escape) {
239
+ mixBits = read(bb, 8); mixRes = (read(bb, 8) << 24) >> 24 // int8
240
+ let hu = read(bb, 8), modeU = hu >> 4, denShiftU = hu & 0xf
241
+ let pu = read(bb, 8), pbFactorU = pu >> 5, numU = pu & 0x1f
242
+ let coefsU = new Int16Array(32)
243
+ for (let i = 0; i < numU; i++) coefsU[i] = read(bb, 16)
244
+ let hv = read(bb, 8), modeV = hv >> 4, denShiftV = hv & 0xf
245
+ let pv = read(bb, 8), pbFactorV = pv >> 5, numV = pv & 0x1f
246
+ let coefsV = new Int16Array(32)
247
+ for (let i = 0; i < numV; i++) coefsV[i] = read(bb, 16)
248
+ let shiftPos = bb.pos
249
+ if (bytesShifted) bb.pos += shift * 2 * numSamples
250
+ dynDecomp(agParams(pbFactorU), bb, pred, numSamples, chanBits)
251
+ if (modeU === 0) unpcBlock(pred, mixU, numSamples, coefsU, numU, chanBits, denShiftU)
252
+ else { unpcBlock(pred, pred, numSamples, null, 31, chanBits, 0); unpcBlock(pred, mixU, numSamples, coefsU, numU, chanBits, denShiftU) }
253
+ dynDecomp(agParams(pbFactorV), bb, pred, numSamples, chanBits)
254
+ if (modeV === 0) unpcBlock(pred, mixV, numSamples, coefsV, numV, chanBits, denShiftV)
255
+ else { unpcBlock(pred, pred, numSamples, null, 31, chanBits, 0); unpcBlock(pred, mixV, numSamples, coefsV, numV, chanBits, denShiftV) }
256
+ readShift(bb, shiftPos, shift, numSamples, 2)
257
+ } else {
258
+ chanBits = cfg.bitDepth
259
+ let s = 32 - chanBits
260
+ if (chanBits <= 16) {
261
+ for (let i = 0; i < numSamples; i++) {
262
+ mixU[i] = (read(bb, chanBits) << s) >> s
263
+ mixV[i] = (read(bb, chanBits) << s) >> s
264
+ }
265
+ } else {
266
+ let extra = chanBits - 16
267
+ for (let i = 0; i < numSamples; i++) {
268
+ let v = (read(bb, 16) << 16) >> s; mixU[i] = v | read(bb, extra)
269
+ let w = (read(bb, 16) << 16) >> s; mixV[i] = w | read(bb, extra)
270
+ }
271
+ }
272
+ bytesShifted = 0
273
+ }
274
+ // un-mix
275
+ let l = new Int32Array(numSamples), r = new Int32Array(numSamples)
276
+ for (let j = 0; j < numSamples; j++) {
277
+ let a, b
278
+ if (mixRes !== 0) { a = mixU[j] + mixV[j] - ((mixRes * mixV[j]) >> mixBits); b = a - mixV[j] }
279
+ else { a = mixU[j]; b = mixV[j] }
280
+ if (bytesShifted) { a = (a << shift) | shiftBuf[2 * j]; b = (b << shift) | shiftBuf[2 * j + 1] }
281
+ l[j] = a; r[j] = b
282
+ }
283
+ chans[chanIndex++] = l; chans[chanIndex++] = r
284
+ } else if (tag === ID_DSE) { skipDSE(bb) }
285
+ else if (tag === ID_FIL) { skipFIL(bb) }
286
+ else if (tag === ID_END) { byteAlign(bb); break }
287
+ else throw Error('ALAC: unsupported element ' + tag)
288
+ if (chanIndex >= chans.length) break
289
+ }
290
+ return numSamples
291
+ }
292
+
293
+ function readShift(bb, shiftPos, shift, numSamples, nch) {
294
+ if (!shift) return
295
+ let sb = { data: bb.data, pos: shiftPos, realLen: bb.realLen }
296
+ let count = numSamples * nch
297
+ for (let i = 0; i < count; i++) shiftBuf[i] = read(sb, shift)
298
+ }
299
+
300
+ function skipFIL(bb) { let count = read(bb, 4); if (count === 15) count += read(bb, 8) - 1; bb.pos += count * 8 }
301
+ function skipDSE(bb) {
302
+ read(bb, 4); let align = readOne(bb)
303
+ let count = read(bb, 8); if (count === 255) count += read(bb, 8)
304
+ if (align) byteAlign(bb)
305
+ bb.pos += count * 8
306
+ }
307
+
308
+ return {
309
+ config: cfg,
310
+ decodeFrame(frame) {
311
+ let data = new Uint8Array(frame.length + 8)
312
+ data.set(frame)
313
+ let bb = bits(data, frame.length)
314
+ let chans = new Array(cfg.numChannels)
315
+ let numSamples = decodeElements(bb, chans, cfg.frameLength)
316
+ let channelData = []
317
+ for (let c = 0; c < cfg.numChannels; c++) {
318
+ let src = chans[c] || new Int32Array(numSamples)
319
+ let f = new Float32Array(numSamples)
320
+ for (let i = 0; i < numSamples; i++) f[i] = src[i] * scale
321
+ channelData.push(f)
322
+ }
323
+ return { channelData, numSamples }
324
+ },
325
+ }
326
+ }
package/decode-aac.js CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * AAC decoder — FAAD2 compiled to WASM
3
- * Decodes M4A (MP4/AAC) and raw ADTS streams
3
+ * Decodes M4A (MP4/AAC) and raw ADTS streams. M4A files carrying ALAC
4
+ * (Apple Lossless) are decoded by the pure-JS ALAC decoder instead.
4
5
  *
5
6
  * let { channelData, sampleRate } = await decode(m4abuf)
6
7
  */
7
8
 
9
+ import { createALAC } from './alac.js'
10
+
8
11
  let _modP
9
12
 
10
13
  async function getMod() {
@@ -61,9 +64,12 @@ class AACDecoder {
61
64
  this._ptr = 0
62
65
  this._cap = 0
63
66
  this._left = null
64
- this._m4a = null // { sizes: number[], idx: number } — M4A streaming state
67
+ this._fileOff = 0 // absolute file offset of _left[0] (M4A streaming)
68
+ this._skip = 0 // bytes to discard from incoming data (M4A streaming, when next frame is past _left)
69
+ this._m4a = null // M4A streaming iterator: { sizes, stco, stsc, idx, ci, sInC, spc, nextOff }
65
70
  this._accum = null // Uint8Array[] — M4A header accumulator
66
71
  this._accumLen = 0
72
+ this._alac = null // ALAC decoder when the M4A carries Apple Lossless
67
73
  }
68
74
 
69
75
  decode(data) {
@@ -72,36 +78,18 @@ class AACDecoder {
72
78
 
73
79
  let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
74
80
 
75
- // M4A streming phase 2: extract frames by known sizes
76
81
  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
82
  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
83
+ // M4A: accumulating moov+mdat header, or first chunk starts with ftyp
84
+ if (this._accum || (buf.length > 8 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70)) {
85
+ (this._accum ??= []).push(buf)
86
+ this._accumLen += buf.length
92
87
  return this._tryM4AInit()
93
88
  }
94
-
95
89
  return this._decodeADTS(buf)
96
90
  }
97
91
 
98
92
  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
93
  this._left = null
106
94
  return EMPTY
107
95
  }
@@ -120,7 +108,8 @@ class AACDecoder {
120
108
  this._cap = 0
121
109
  }
122
110
  this._accum = null; this._accumLen = 0
123
- this._m4a = null; this._left = null
111
+ this._m4a = null; this._left = null; this._alac = null
112
+ this._fileOff = 0; this._skip = 0
124
113
  }
125
114
 
126
115
  _catAccum() {
@@ -132,73 +121,84 @@ class AACDecoder {
132
121
 
133
122
  _tryM4AInit() {
134
123
  let buf = this._catAccum()
135
- let asc = null, stsz = null, stco = null, stsc = null
136
- let mdatOff = 0, mdatLen = 0
124
+ let asc = null, alacCookie = null, stsz = null, stco = null, stsc = null
137
125
 
138
- parseBoxes(buf, 0, buf.length, (type, data, off) => {
126
+ parseBoxes(buf, 0, buf.length, (type, data) => {
139
127
  if (type === 'esds') asc = parseEsds(data)
128
+ else if (type === 'alac') alacCookie = data // ALAC magic cookie: version/flags(4) + ALACSpecificConfig(24)
140
129
  else if (type === 'stsz') stsz = parseStsz(data)
141
130
  else if (type === 'stco') stco = parseStco(data)
142
131
  else if (type === 'co64') stco = parseCo64(data)
143
132
  else if (type === 'stsc') stsc = parseStsc(data)
144
- else if (type === 'mdat') { mdatOff = off; mdatLen = data.length }
145
133
  })
146
134
 
147
- if (!asc) return EMPTY // moov not found yet
148
-
149
- // Init WASM decoder with ASC
150
- let m = this.m, h = m._aac_create()
151
- let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
152
- let ptr = this._alloc(asc.length)
153
- m.HEAPU8.set(asc, ptr)
154
- let err = m._aac_init2(h, ptr, asc.length, srP, chP)
155
- if (err < 0) { m._aac_close(h); throw Error('M4A init failed (code ' + err + ')') }
156
- this.sr = m.getValue(srP, 'i32')
157
- this.ch = m.getValue(chP, 'i8')
158
- if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
159
- this.h = h
160
-
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 }
168
- this._accum = null; this._accumLen = 0
135
+ if ((!asc && !alacCookie) || !stsz || !stco?.length) return EMPTY // moov/tables not ready
169
136
 
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()
137
+ if (alacCookie) {
138
+ // ALAC (Apple Lossless) — pure JS, no FAAD2
139
+ this._alac = createALAC(alacCookie.subarray(4))
140
+ this.sr = this._alac.config.sampleRate
141
+ this.ch = this._alac.config.numChannels
142
+ } else {
143
+ // Init WASM decoder with ASC
144
+ let m = this.m, h = m._aac_create()
145
+ let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
146
+ let ptr = this._alloc(asc.length)
147
+ m.HEAPU8.set(asc, ptr)
148
+ let err = m._aac_init2(h, ptr, asc.length, srP, chP)
149
+ if (err < 0) { m._aac_close(h); throw Error('M4A init failed (code ' + err + ')') }
150
+ this.sr = m.getValue(srP, 'i32')
151
+ this.ch = m.getValue(chP, 'i8')
152
+ if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
153
+ this.h = h
176
154
  }
177
155
 
178
- if (!frames.length) return EMPTY
179
- return this._feedFrames(frames)
156
+ // Streaming: walk sample tables by absolute file offset so chunk boundaries are irrelevant.
157
+ this._accum = null; this._accumLen = 0
158
+ this._m4a = { sizes: stsz, stco, stsc, idx: 0, ci: 0, sInC: 0, spc: spcAt(0, stsc), nextOff: stco[0] }
159
+ this._left = buf
160
+ this._fileOff = 0
161
+ this._skip = 0
162
+ return this._extractM4A()
180
163
  }
181
164
 
182
165
  _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
166
+ if (this._skip > 0) {
167
+ let n = Math.min(this._skip, buf.length)
168
+ this._skip -= n
169
+ this._fileOff += n
170
+ buf = buf.subarray(n)
171
+ if (!buf.length) return EMPTY
188
172
  }
173
+ this._left = append(this._left, buf)
174
+ return this._extractM4A()
175
+ }
189
176
 
190
- let frames = [], pos = 0
177
+ _extractM4A() {
178
+ let st = this._m4a, frames = []
191
179
  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++
180
+ let off = st.nextOff, sz = st.sizes[st.idx]
181
+ let bufOff = off - this._fileOff
182
+ if (bufOff + sz > this._left.length) break
183
+ if (bufOff >= 0) frames.push(this._left.subarray(bufOff, bufOff + sz))
184
+ advanceM4A(st)
197
185
  }
198
186
 
199
- if (pos < buf.length) this._left = buf.subarray(pos).slice()
200
- if (!frames.length) return EMPTY
201
- return this._feedFrames(frames)
187
+ if (st.idx < st.sizes.length) {
188
+ let nextOff = st.nextOff, end = this._fileOff + this._left.length
189
+ if (nextOff >= end) {
190
+ this._skip = nextOff - end
191
+ this._fileOff = end
192
+ this._left = null
193
+ } else if (nextOff > this._fileOff) {
194
+ this._left = this._left.subarray(nextOff - this._fileOff).slice()
195
+ this._fileOff = nextOff
196
+ }
197
+ } else {
198
+ this._left = null
199
+ }
200
+
201
+ return frames.length ? this._feedFrames(frames) : EMPTY
202
202
  }
203
203
 
204
204
  _alloc(len) {
@@ -213,14 +213,7 @@ class AACDecoder {
213
213
  _decodeADTS(buf) {
214
214
  let m = this.m
215
215
 
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
- }
216
+ if (this._left) { buf = append(this._left, buf); this._left = null }
224
217
 
225
218
  if (!this.h) {
226
219
  if (buf.length < 7) { this._left = buf.slice(); return EMPTY }
@@ -261,28 +254,8 @@ class AACDecoder {
261
254
  return this._feedFrames(frames)
262
255
  }
263
256
 
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
257
  _feedFrames(frames) {
258
+ if (this._alac) return this._feedALAC(frames)
286
259
  let m = this.m, h = this.h
287
260
  let chunks = [], totalPerCh = 0, channels = this.ch, errors = 0
288
261
 
@@ -317,31 +290,34 @@ class AACDecoder {
317
290
 
318
291
  return { channelData, sampleRate: this.sr, errors }
319
292
  }
293
+
294
+ _feedALAC(frames) {
295
+ let channels = this.ch, parts = [], total = 0
296
+ for (let frame of frames) {
297
+ let r = this._alac.decodeFrame(frame)
298
+ parts.push(r.channelData)
299
+ total += r.numSamples
300
+ }
301
+ if (!total) return EMPTY
302
+ let channelData = Array.from({ length: channels }, () => new Float32Array(total))
303
+ let pos = 0
304
+ for (let cd of parts) {
305
+ let n = cd[0].length
306
+ for (let c = 0; c < channels; c++) channelData[c].set(cd[c], pos)
307
+ pos += n
308
+ }
309
+ return { channelData, sampleRate: this.sr }
310
+ }
320
311
  }
321
312
 
322
313
 
323
314
  // ===== M4A demuxer =====
324
315
 
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 }
316
+ function append(left, buf) {
317
+ if (!left?.length) return buf.slice()
318
+ let merged = new Uint8Array(left.length + buf.length)
319
+ merged.set(left); merged.set(buf, left.length)
320
+ return merged
345
321
  }
346
322
 
347
323
  const CONTAINERS = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl', 'udta', 'meta', 'edts', 'sinf'])
@@ -352,28 +328,21 @@ function parseBoxes(buf, start, end, cb) {
352
328
  let size = r32(buf, off)
353
329
  let type = String.fromCharCode(buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7])
354
330
 
355
- if (size === 0) {
356
- size = end - off
357
- } else if (size === 1 && off + 16 <= end) {
331
+ if (size === 0) size = end - off
332
+ else if (size === 1 && off + 16 <= end) {
358
333
  size = r32(buf, off + 12)
359
334
  if (size < 16) break
360
- } else if (size < 8) {
361
- break
362
- }
335
+ } else if (size < 8) break
363
336
 
364
- let bodyOff = off + 8
365
- let truncated = off + size > end
337
+ // skip mdat fast — the raw frames don't interest us here
338
+ if (type === 'mdat') { off += size; continue }
339
+ // truncated non-mdat box: tables would be garbage — wait for more data
340
+ if (off + size > end) break
366
341
 
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)
342
+ let bodyOff = off + 8
343
+ if (type === 'stsd') parseSampleDesc(buf, bodyOff, size - 8, cb)
375
344
  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)
345
+ else cb(type, buf.subarray(bodyOff, off + size))
377
346
 
378
347
  off += size
379
348
  }
@@ -384,7 +353,8 @@ function parseSampleDesc(buf, off, len, cb) {
384
353
  for (let i = 0; i < entries && pos < off + len; i++) {
385
354
  let eSize = r32(buf, pos)
386
355
  let eType = String.fromCharCode(buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7])
387
- if (eType === 'mp4a' && eSize > 36) parseBoxes(buf, pos + 36, pos + eSize, cb)
356
+ // recurse into the audio sample entry so its child boxes (esds for AAC, alac cookie for ALAC) surface
357
+ if ((eType === 'mp4a' || eType === 'alac') && eSize > 36) parseBoxes(buf, pos + 36, pos + eSize, cb)
388
358
  pos += eSize
389
359
  }
390
360
  }
@@ -428,41 +398,24 @@ function parseStsc(data) {
428
398
  return e
429
399
  }
430
400
 
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
401
+ function spcAt(ci, stsc) {
402
+ if (!stsc?.length) return 1
403
+ let spc = 1, cn = ci + 1
404
+ for (let j = stsc.length - 1; j >= 0; j--)
405
+ if (cn >= stsc[j].first) { spc = stsc[j].spc; break }
406
+ return spc
450
407
  }
451
408
 
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++
409
+ function advanceM4A(st) {
410
+ st.nextOff += st.sizes[st.idx]
411
+ st.idx++
412
+ st.sInC++
413
+ if (st.sInC >= st.spc && st.ci + 1 < st.stco.length) {
414
+ st.ci++
415
+ st.sInC = 0
416
+ st.spc = spcAt(st.ci, st.stsc)
417
+ st.nextOff = st.stco[st.ci]
464
418
  }
465
- return frames
466
419
  }
467
420
 
468
421
  function r32(buf, off) {
package/meta.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * M4A / MP4 metadata — iTunes-style ilst tags + cover art.
3
+ * @module @audio/decode-aac/meta
4
+ *
5
+ * import { parseMeta } from '@audio/decode-aac/meta'
6
+ * let { meta, sampleRate } = parseMeta(m4aBytes)
7
+ * // meta: { title, artist, album, year, genre, track, ..., pictures, raw }
8
+ */
9
+
10
+ const TD = new TextDecoder('utf-8')
11
+ const C = String.fromCharCode(0xA9) // © prefix used by iTunes text atoms
12
+
13
+ function r32(b, o) { return (b[o] * 0x1000000) + (b[o + 1] << 16) + (b[o + 2] << 8) + b[o + 3] }
14
+ function r16(b, o) { return (b[o] << 8) | b[o + 1] }
15
+ function typ(b, o) { return String.fromCharCode(b[o], b[o + 1], b[o + 2], b[o + 3]) }
16
+
17
+ const ILST_MAP = {
18
+ [C + 'nam']: 'title', [C + 'ART']: 'artist', aART: 'albumartist', [C + 'alb']: 'album',
19
+ [C + 'day']: 'year', [C + 'gen']: 'genre', [C + 'wrt']: 'composer', [C + 'cmt']: 'comment',
20
+ [C + 'too']: 'software', cprt: 'copyright', [C + 'lyr']: 'lyrics'
21
+ }
22
+
23
+ const CONTAINERS = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl', 'udta', 'meta'])
24
+
25
+ function applyTag(name, flags, payload, meta, pictures) {
26
+ if (name === 'covr') {
27
+ let mime = flags === 14 ? 'image/png' : flags === 13 ? 'image/jpeg' : 'application/octet-stream'
28
+ pictures.push({ mime, type: 3, description: '', data: payload.slice() })
29
+ return
30
+ }
31
+ if (name === 'trkn') { if (payload.length >= 4) meta.track = String(r16(payload, 2)); return }
32
+ if (name === 'disk') { if (payload.length >= 4) meta.disc = String(r16(payload, 2)); return }
33
+ if (name === 'tmpo') { if (payload.length >= 2) meta.bpm = String(r16(payload, 0)); return }
34
+ let key = ILST_MAP[name]
35
+ if (key && meta[key] == null) meta[key] = TD.decode(payload).replace(/\0+$/, '')
36
+ }
37
+
38
+ // ilst children are tag atoms; each holds a 'data' sub-atom: type(4) + flags(4) + reserved(4) + value
39
+ function parseIlst(b, start, end, meta, pictures) {
40
+ let off = start
41
+ while (off + 8 <= end) {
42
+ let size = r32(b, off); if (size < 8) break
43
+ let name = typ(b, off + 4), aEnd = Math.min(off + size, end), p = off + 8
44
+ while (p + 16 <= aEnd) {
45
+ let dsize = r32(b, p); if (dsize < 16) break
46
+ if (typ(b, p + 4) === 'data') {
47
+ let flags = r32(b, p + 8) & 0xFFFFFF
48
+ applyTag(name, flags, b.subarray(p + 16, Math.min(p + dsize, aEnd)), meta, pictures)
49
+ break
50
+ }
51
+ p += dsize
52
+ }
53
+ off += size
54
+ }
55
+ }
56
+
57
+ function walk(b, start, end, ctx) {
58
+ let off = start
59
+ while (off + 8 <= end) {
60
+ let size = r32(b, off), type = typ(b, off + 4)
61
+ if (size === 0) size = end - off
62
+ else if (size === 1) break // 64-bit box — not expected in moov metadata
63
+ if (size < 8 || off + size > end) break
64
+ let bodyOff = off + 8
65
+ if (type === 'ilst') parseIlst(b, bodyOff, off + size, ctx.meta, ctx.pictures)
66
+ else if (type === 'stsd') walk(b, bodyOff + 8, off + size, ctx) // FullBox(4) + entryCount(4)
67
+ else if ((type === 'mp4a' || type === 'alac') && !ctx.sampleRate) ctx.sampleRate = r16(b, bodyOff + 24)
68
+ else if (CONTAINERS.has(type)) walk(b, bodyOff + (type === 'meta' ? 4 : 0), off + size, ctx)
69
+ off += size
70
+ }
71
+ }
72
+
73
+ /** Parse M4A/MP4 metadata. Returns { meta, sampleRate, markers, regions } or null. */
74
+ export function parseMeta(bytes) {
75
+ let b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
76
+ if (b.length < 8 || typ(b, 4) !== 'ftyp') return null
77
+ let ctx = { meta: {}, pictures: [], sampleRate: 0 }
78
+ walk(b, 0, b.length, ctx)
79
+ ctx.meta.pictures = ctx.pictures
80
+ return { meta: ctx.meta, sampleRate: ctx.sampleRate, markers: [], regions: [] }
81
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@audio/decode-aac",
3
- "version": "1.1.2",
4
- "description": "Decode AAC/M4A audio via FAAD2 WASM",
3
+ "version": "1.2.0",
4
+ "description": "Decode AAC/M4A (FAAD2 WASM) and ALAC/Apple Lossless (pure JS) to PCM samples",
5
5
  "type": "module",
6
6
  "main": "decode-aac.js",
7
7
  "types": "decode-aac.d.ts",
8
8
  "exports": {
9
9
  ".": "./decode-aac.js",
10
+ "./meta": "./meta.js",
10
11
  "./package.json": "./package.json"
11
12
  },
12
13
  "publishConfig": {
@@ -19,6 +20,8 @@
19
20
  },
20
21
  "files": [
21
22
  "decode-aac.js",
23
+ "alac.js",
24
+ "meta.js",
22
25
  "decode-aac.d.ts",
23
26
  "src/aac.wasm.cjs",
24
27
  "LICENSE"
@@ -31,7 +34,10 @@
31
34
  "decode",
32
35
  "decoder",
33
36
  "wasm",
34
- "faad2"
37
+ "faad2",
38
+ "alac",
39
+ "apple lossless",
40
+ "lossless"
35
41
  ],
36
42
  "license": "GPL-2.0",
37
43
  "author": "audiojs",