@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.
- package/README.md +13 -4
- package/alac.js +326 -0
- package/decode-aac.js +125 -172
- 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() {
|
|
@@ -61,9 +64,12 @@ class AACDecoder {
|
|
|
61
64
|
this._ptr = 0
|
|
62
65
|
this._cap = 0
|
|
63
66
|
this._left = null
|
|
64
|
-
this.
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
this.
|
|
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
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
buf =
|
|
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
|
-
|
|
177
|
+
_extractM4A() {
|
|
178
|
+
let st = this._m4a, frames = []
|
|
191
179
|
while (st.idx < st.sizes.length) {
|
|
192
|
-
let sz = st.sizes[st.idx]
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
st
|
|
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 (
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
|
|
327
|
-
let
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
368
|
-
if (type === '
|
|
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)
|
|
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
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (stsc
|
|
436
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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.
|
|
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",
|