@audio/decode-aac 1.1.3 → 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.
- package/README.md +13 -4
- package/alac.js +326 -0
- package/decode-aac.js +48 -16
- package/meta.js +81 -0
- 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.
|
|
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
|
-
|
|
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() {
|
|
@@ -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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audio/decode-aac",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Decode AAC/M4A
|
|
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",
|