@audio/decode-caf 1.1.1 → 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 CHANGED
@@ -47,6 +47,7 @@ Creates a decoder instance.
47
47
  - `lpcm` — 8, 16, 24, 32-bit signed integer (LE/BE), 32/64-bit float (LE/BE)
48
48
  - `alaw` — G.711 A-law
49
49
  - `ulaw` — G.711 mu-law
50
+ - `ima4` — IMA/QuickTime ADPCM (iOS Voice Memos, GarageBand)
50
51
 
51
52
  ## License
52
53
 
package/decode-caf.js CHANGED
@@ -7,6 +7,10 @@
7
7
 
8
8
  const EMPTY = Object.freeze({ channelData: [], sampleRate: 0 })
9
9
 
10
+ // IMA/QuickTime ADPCM tables
11
+ const IMA_STEP = new Int16Array([7,8,9,10,11,12,13,14,16,17,19,21,23,25,28,31,34,37,41,45,50,55,60,66,73,80,88,97,107,118,130,143,157,173,190,209,230,253,279,307,337,371,408,449,494,544,598,658,724,796,876,963,1060,1166,1282,1411,1552,1707,1878,2066,2272,2499,2749,3024,3327,3660,4026,4428,4871,5358,5894,6484,7132,7845,8630,9493,10442,11487,12635,13899,15289,16818,18500,20350,22385,24623,27086,29794,32767])
12
+ const IMA_IDX = new Int8Array([-1,-1,-1,-1,2,4,6,8,-1,-1,-1,-1,2,4,6,8])
13
+
10
14
  /**
11
15
  * Whole-file decode
12
16
  * @param {Uint8Array|ArrayBuffer} src
@@ -23,7 +27,7 @@ export default async function decode(src) {
23
27
  * @returns {Promise<{decode(chunk: Uint8Array): {channelData, sampleRate}, flush(), free()}>}
24
28
  */
25
29
  export async function decoder() {
26
- let hdr = null, left = null, freed = false
30
+ let hdr = null, left = null, freed = false, dataLeft = Infinity
27
31
  return {
28
32
  decode(data) {
29
33
  if (freed) throw Error('Decoder already freed')
@@ -33,12 +37,17 @@ export async function decoder() {
33
37
  if (!hdr) {
34
38
  hdr = scanCafHdr(chunk)
35
39
  if (!hdr) { left = chunk.slice(); return EMPTY }
40
+ dataLeft = hdr.dataSize
36
41
  chunk = chunk.subarray(hdr.dataStart)
37
42
  }
43
+ if (dataLeft <= 0) return EMPTY // past the data chunk — ignore trailing chunks
38
44
  let fb = hdr.frameBytes
39
- let complete = Math.floor(chunk.length / fb) * fb
40
- if (!complete) { if (chunk.length) left = chunk.slice(); return EMPTY }
41
- if (chunk.length > complete) left = chunk.subarray(complete).slice()
45
+ let avail = Math.min(chunk.length, dataLeft) // don't read past the declared data size
46
+ let complete = Math.floor(avail / fb) * fb
47
+ if (!complete) { if (chunk.length && dataLeft > chunk.length) left = chunk.slice(); return EMPTY }
48
+ dataLeft -= complete
49
+ let rest = chunk.subarray(complete)
50
+ if (rest.length && dataLeft > 0) left = rest.subarray(0, Math.min(rest.length, dataLeft)).slice()
42
51
  return decodeCafRaw(chunk.subarray(0, complete), hdr)
43
52
  },
44
53
  flush() { left = null; return EMPTY },
@@ -78,13 +87,16 @@ function scanCafHdr(buf) {
78
87
  if (!desc) return null
79
88
  let dataStart = off + 4 // skip editCount
80
89
  if (dataStart > buf.length) return null
90
+ // -1 (0xFFFF…) size means "until EOF" — read to end; else payload minus 4-byte editCount
91
+ let dataSize = (sizeHi === 0xFFFFFFFF && sizeLo === 0xFFFFFFFF) ? Infinity : Math.max(0, size - 4)
81
92
  let { formatID, formatFlags, channelsPerFrame: ch, bitsPerChannel: bits } = desc
82
93
  let bytesPerSample = bits >> 3
83
94
  let frameBytes
84
95
  if (formatID === 'alaw' || formatID === 'ulaw') frameBytes = ch
96
+ else if (formatID === 'ima4') frameBytes = desc.bytesPerPacket || 34 * ch // packet = 34 bytes/channel
85
97
  else frameBytes = ch * bytesPerSample
86
98
  if (!frameBytes) return null
87
- return { ...desc, dataStart, frameBytes }
99
+ return { ...desc, dataStart, frameBytes, dataSize }
88
100
  }
89
101
  if (size < 0) break
90
102
  if (sizeHi === 0xFFFFFFFF && sizeLo === 0xFFFFFFFF) break
@@ -101,10 +113,53 @@ function decodeCafRaw(raw, hdr) {
101
113
  if (formatID === 'lpcm') samples = decodeLPCM(raw, formatFlags, bits, ch)
102
114
  else if (formatID === 'alaw') samples = decodeAlaw(raw, ch)
103
115
  else if (formatID === 'ulaw') samples = decodeUlaw(raw, ch)
116
+ else if (formatID === 'ima4') samples = decodeIma4(raw, hdr)
104
117
  else throw Error('CAF: unsupported format ' + formatID)
105
118
  return { channelData: samples, sampleRate }
106
119
  }
107
120
 
121
+ // QuickTime IMA4 ADPCM — 34-byte packets per channel → 64 frames each.
122
+ // The predictor runs continuously across the whole stream; each packet's 2-byte
123
+ // preamble only snapshots the high 9 bits, so resetting per packet would lose up
124
+ // to 127 LSB. State (predictor + step index) is carried on hdr across chunks.
125
+ function decodeIma4(raw, hdr) {
126
+ let nCh = hdr.channelsPerFrame, fpp = hdr.framesPerPacket || 64
127
+ let pktBytes = 34 * nCh, nPk = Math.floor(raw.length / pktBytes)
128
+ if (!nPk) return []
129
+ let st = (hdr.ima ||= { pred: new Int32Array(nCh), step: new Int32Array(nCh), started: false })
130
+ let init = !st.started
131
+ let ch = Array.from({ length: nCh }, () => new Float32Array(nPk * fpp))
132
+ for (let pk = 0; pk < nPk; pk++) {
133
+ for (let c = 0; c < nCh; c++) {
134
+ let bp = pk * pktBytes + c * 34
135
+ if (init && pk === 0) {
136
+ let pre = (raw[bp] << 8) | raw[bp + 1]
137
+ let p = pre & 0xFF80; if (p > 32767) p -= 65536
138
+ st.pred[c] = p
139
+ st.step[c] = Math.min(pre & 0x7F, 88)
140
+ }
141
+ let pred = st.pred[c], ix = st.step[c], out = ch[c], base = pk * fpp
142
+ for (let i = 0; i < 32; i++) {
143
+ let byte = raw[bp + 2 + i]
144
+ for (let h = 0; h < 2; h++) {
145
+ let nib = h === 0 ? byte & 0x0F : (byte >> 4) & 0x0F
146
+ let step = IMA_STEP[ix], diff = step >> 3
147
+ if (nib & 1) diff += step >> 2
148
+ if (nib & 2) diff += step >> 1
149
+ if (nib & 4) diff += step
150
+ pred += (nib & 8) ? -diff : diff
151
+ if (pred > 32767) pred = 32767; else if (pred < -32768) pred = -32768
152
+ ix += IMA_IDX[nib]; if (ix < 0) ix = 0; else if (ix > 88) ix = 88
153
+ out[base + i * 2 + h] = pred / 32768
154
+ }
155
+ }
156
+ st.pred[c] = pred; st.step[c] = ix
157
+ }
158
+ }
159
+ st.started = true
160
+ return ch
161
+ }
162
+
108
163
  function decodeLPCM(data, flags, bits, ch) {
109
164
  let isFloat = flags & 1, isLE = flags & 2
110
165
  let bytesPerSample = bits >> 3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audio/decode-caf",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Decode CAF (Core Audio Format) audio to PCM samples",
5
5
  "type": "module",
6
6
  "main": "decode-caf.js",