@audio/decode-aac 1.1.3 → 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.
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/audio.js ADDED
@@ -0,0 +1,10 @@
1
+ // audio.js manifest — codec atom, decode half: whole-buffer bytes → { channelData,
2
+ // sampleRate }. Hosts merge with the encode half (@audio/encode-aac) by format
3
+ // name. Format detection is magic-byte (audio-type) — no test() needed here.
4
+
5
+ import decodeFn from './decode-aac.js'
6
+
7
+ export const aac = {
8
+ codec: 'aac',
9
+ decode: (bytes) => decodeFn(bytes),
10
+ }
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() {
@@ -66,6 +69,7 @@ class AACDecoder {
66
69
  this._m4a = null // M4A streaming iterator: { sizes, stco, stsc, idx, ci, sInC, spc, nextOff }
67
70
  this._accum = null // Uint8Array[] — M4A header accumulator
68
71
  this._accumLen = 0
72
+ this._alac = null // ALAC decoder when the M4A carries Apple Lossless
69
73
  }
70
74
 
71
75
  decode(data) {
@@ -104,7 +108,7 @@ class AACDecoder {
104
108
  this._cap = 0
105
109
  }
106
110
  this._accum = null; this._accumLen = 0
107
- this._m4a = null; this._left = null
111
+ this._m4a = null; this._left = null; this._alac = null
108
112
  this._fileOff = 0; this._skip = 0
109
113
  }
110
114
 
@@ -117,29 +121,37 @@ class AACDecoder {
117
121
 
118
122
  _tryM4AInit() {
119
123
  let buf = this._catAccum()
120
- let asc = null, stsz = null, stco = null, stsc = null
124
+ let asc = null, alacCookie = null, stsz = null, stco = null, stsc = null
121
125
 
122
126
  parseBoxes(buf, 0, buf.length, (type, data) => {
123
127
  if (type === 'esds') asc = parseEsds(data)
128
+ else if (type === 'alac') alacCookie = data // ALAC magic cookie: version/flags(4) + ALACSpecificConfig(24)
124
129
  else if (type === 'stsz') stsz = parseStsz(data)
125
130
  else if (type === 'stco') stco = parseStco(data)
126
131
  else if (type === 'co64') stco = parseCo64(data)
127
132
  else if (type === 'stsc') stsc = parseStsc(data)
128
133
  })
129
134
 
130
- if (!asc || !stsz || !stco?.length) return EMPTY // moov/tables not ready
135
+ if ((!asc && !alacCookie) || !stsz || !stco?.length) return EMPTY // moov/tables not ready
131
136
 
132
- // Init WASM decoder with ASC
133
- let m = this.m, h = m._aac_create()
134
- let srP = m._aac_sr_ptr(), chP = m._aac_ch_ptr()
135
- let ptr = this._alloc(asc.length)
136
- m.HEAPU8.set(asc, ptr)
137
- let err = m._aac_init2(h, ptr, asc.length, srP, chP)
138
- if (err < 0) { m._aac_close(h); throw Error('M4A init failed (code ' + err + ')') }
139
- this.sr = m.getValue(srP, 'i32')
140
- this.ch = m.getValue(chP, 'i8')
141
- if (!this.ch) { m._aac_close(h); throw Error('M4A init: no channels in ASC') }
142
- this.h = h
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
154
+ }
143
155
 
144
156
  // Streaming: walk sample tables by absolute file offset so chunk boundaries are irrelevant.
145
157
  this._accum = null; this._accumLen = 0
@@ -243,6 +255,7 @@ class AACDecoder {
243
255
  }
244
256
 
245
257
  _feedFrames(frames) {
258
+ if (this._alac) return this._feedALAC(frames)
246
259
  let m = this.m, h = this.h
247
260
  let chunks = [], totalPerCh = 0, channels = this.ch, errors = 0
248
261
 
@@ -277,6 +290,24 @@ class AACDecoder {
277
290
 
278
291
  return { channelData, sampleRate: this.sr, errors }
279
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
+ }
280
311
  }
281
312
 
282
313
 
@@ -322,7 +353,8 @@ function parseSampleDesc(buf, off, len, cb) {
322
353
  for (let i = 0; i < entries && pos < off + len; i++) {
323
354
  let eSize = r32(buf, pos)
324
355
  let eType = String.fromCharCode(buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7])
325
- 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)
326
358
  pos += eSize
327
359
  }
328
360
  }
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,14 @@
1
1
  {
2
2
  "name": "@audio/decode-aac",
3
- "version": "1.1.3",
4
- "description": "Decode AAC/M4A audio via FAAD2 WASM",
3
+ "version": "1.3.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",
11
+ "./audio": "./audio.js",
10
12
  "./package.json": "./package.json"
11
13
  },
12
14
  "publishConfig": {
@@ -19,9 +21,12 @@
19
21
  },
20
22
  "files": [
21
23
  "decode-aac.js",
24
+ "alac.js",
25
+ "meta.js",
22
26
  "decode-aac.d.ts",
23
27
  "src/aac.wasm.cjs",
24
- "LICENSE"
28
+ "LICENSE",
29
+ "audio.js"
25
30
  ],
26
31
  "keywords": [
27
32
  "aac",
@@ -31,7 +36,10 @@
31
36
  "decode",
32
37
  "decoder",
33
38
  "wasm",
34
- "faad2"
39
+ "faad2",
40
+ "alac",
41
+ "apple lossless",
42
+ "lossless"
35
43
  ],
36
44
  "license": "GPL-2.0",
37
45
  "author": "audiojs",
@@ -43,5 +51,6 @@
43
51
  },
44
52
  "engines": {
45
53
  "node": ">=16"
46
- }
54
+ },
55
+ "audio": "./audio.js"
47
56
  }
package/src/aac.wasm.cjs CHANGED
Binary file