@audio/decode-caf 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/decode-caf.js +53 -52
  2. package/package.json +7 -3
package/decode-caf.js CHANGED
@@ -19,50 +19,52 @@ export default async function decode(src) {
19
19
  }
20
20
 
21
21
  /**
22
- * Create decoder instance
22
+ * Create decoder instance (streaming-aware)
23
23
  * @returns {Promise<{decode(chunk: Uint8Array): {channelData, sampleRate}, flush(), free()}>}
24
24
  */
25
25
  export async function decoder() {
26
- return new CAFDecoder()
27
- }
28
-
29
- class CAFDecoder {
30
- constructor() { this.done = false }
31
-
32
- decode(data) {
33
- if (this.done) throw Error('Decoder already freed')
34
- if (!data || !data.byteLength) return EMPTY
35
-
36
- let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
37
- if (buf.length < 8) return EMPTY
38
-
39
- return decodeCAF(buf)
26
+ let hdr = null, left = null, freed = false
27
+ return {
28
+ decode(data) {
29
+ if (freed) throw Error('Decoder already freed')
30
+ if (!data || !data.byteLength) return EMPTY
31
+ let chunk = data instanceof Uint8Array ? data : new Uint8Array(data)
32
+ if (left) { chunk = catB(left, chunk); left = null }
33
+ if (!hdr) {
34
+ hdr = scanCafHdr(chunk)
35
+ if (!hdr) { left = chunk.slice(); return EMPTY }
36
+ chunk = chunk.subarray(hdr.dataStart)
37
+ }
38
+ 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()
42
+ return decodeCafRaw(chunk.subarray(0, complete), hdr)
43
+ },
44
+ flush() { left = null; return EMPTY },
45
+ free() { freed = true; left = null; hdr = null },
40
46
  }
41
-
42
- flush() { return EMPTY }
43
-
44
- free() { this.done = true }
45
47
  }
46
48
 
47
- function decodeCAF(buf) {
48
- let dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
49
+ function catB(a, b) {
50
+ let r = new Uint8Array(a.length + b.length)
51
+ r.set(a); r.set(b, a.length)
52
+ return r
53
+ }
49
54
 
50
- // File header: 'caff'(4) + version(2) + flags(2)
55
+ function scanCafHdr(buf) {
56
+ if (buf.length < 8) return null
51
57
  if (buf[0] !== 0x63 || buf[1] !== 0x61 || buf[2] !== 0x66 || buf[3] !== 0x66) throw Error('Not a CAF file')
58
+ let dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
52
59
  if (dv.getUint16(4, false) !== 1) throw Error('Unsupported CAF version')
53
-
54
- let off = 8, desc = null, dataStart = -1, dataLen = -1
55
-
56
- // Parse chunks
60
+ let off = 8, desc = null
57
61
  while (off + 12 <= buf.length) {
58
62
  let type = String.fromCharCode(buf[off], buf[off + 1], buf[off + 2], buf[off + 3])
59
- // size is int64 BE read high/low 32
60
- let sizeHi = dv.getUint32(off + 4, false)
61
- let sizeLo = dv.getUint32(off + 8, false)
63
+ let sizeHi = dv.getUint32(off + 4, false), sizeLo = dv.getUint32(off + 8, false)
62
64
  let size = sizeHi * 0x100000000 + sizeLo
63
65
  off += 12
64
-
65
- if (type === 'desc' && off + 32 <= buf.length) {
66
+ if (type === 'desc') {
67
+ if (off + 32 > buf.length) return null
66
68
  desc = {
67
69
  sampleRate: dv.getFloat64(off, false),
68
70
  formatID: String.fromCharCode(buf[off + 8], buf[off + 9], buf[off + 10], buf[off + 11]),
@@ -70,37 +72,36 @@ function decodeCAF(buf) {
70
72
  bytesPerPacket: dv.getUint32(off + 16, false),
71
73
  framesPerPacket: dv.getUint32(off + 20, false),
72
74
  channelsPerFrame: dv.getUint32(off + 24, false),
73
- bitsPerChannel: dv.getUint32(off + 28, false)
75
+ bitsPerChannel: dv.getUint32(off + 28, false),
74
76
  }
75
77
  } else if (type === 'data') {
76
- // skip 4-byte editCount
77
- dataStart = off + 4
78
- // size -1 (0xFFFFFFFFFFFFFFFF) means rest of file
79
- dataLen = (sizeHi === 0xFFFFFFFF && sizeLo === 0xFFFFFFFF) ? buf.length - dataStart : size - 4
78
+ if (!desc) return null
79
+ let dataStart = off + 4 // skip editCount
80
+ if (dataStart > buf.length) return null
81
+ let { formatID, formatFlags, channelsPerFrame: ch, bitsPerChannel: bits } = desc
82
+ let bytesPerSample = bits >> 3
83
+ let frameBytes
84
+ if (formatID === 'alaw' || formatID === 'ulaw') frameBytes = ch
85
+ else frameBytes = ch * bytesPerSample
86
+ if (!frameBytes) return null
87
+ return { ...desc, dataStart, frameBytes }
80
88
  }
81
-
82
89
  if (size < 0) break
83
- // -1 size: skip to end
84
90
  if (sizeHi === 0xFFFFFFFF && sizeLo === 0xFFFFFFFF) break
85
91
  off += size
86
92
  }
93
+ return null
94
+ }
87
95
 
88
- if (!desc) throw Error('CAF: missing desc chunk')
89
- if (dataStart < 0) throw Error('CAF: missing data chunk')
90
- if (!desc.channelsPerFrame) throw Error('CAF: 0 channels')
91
- if (!desc.sampleRate) throw Error('CAF: 0 sample rate')
92
-
93
- let audioEnd = Math.min(dataStart + dataLen, buf.length)
94
- let audioData = buf.subarray(dataStart, audioEnd)
95
-
96
- let { formatID, formatFlags, channelsPerFrame: ch, bitsPerChannel: bits, sampleRate } = desc
97
-
96
+ function decodeCafRaw(raw, hdr) {
97
+ let { sampleRate, formatID, formatFlags, channelsPerFrame: ch, bitsPerChannel: bits, frameBytes } = hdr
98
+ let frames = Math.floor(raw.length / frameBytes)
99
+ if (!frames) return EMPTY
98
100
  let samples
99
- if (formatID === 'lpcm') samples = decodeLPCM(audioData, formatFlags, bits, ch)
100
- else if (formatID === 'alaw') samples = decodeAlaw(audioData, ch)
101
- else if (formatID === 'ulaw') samples = decodeUlaw(audioData, ch)
101
+ if (formatID === 'lpcm') samples = decodeLPCM(raw, formatFlags, bits, ch)
102
+ else if (formatID === 'alaw') samples = decodeAlaw(raw, ch)
103
+ else if (formatID === 'ulaw') samples = decodeUlaw(raw, ch)
102
104
  else throw Error('CAF: unsupported format ' + formatID)
103
-
104
105
  return { channelData: samples, sampleRate }
105
106
  }
106
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audio/decode-caf",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Decode CAF (Core Audio Format) audio to PCM samples",
5
5
  "type": "module",
6
6
  "main": "decode-caf.js",
@@ -26,7 +26,9 @@
26
26
  "decoder",
27
27
  "pcm"
28
28
  ],
29
- "publishConfig": { "access": "public" },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
30
32
  "license": "MIT",
31
33
  "author": "audiojs",
32
34
  "homepage": "https://github.com/audiojs/decode-caf#readme",
@@ -34,6 +36,8 @@
34
36
  "type": "git",
35
37
  "url": "git+https://github.com/audiojs/decode-caf.git"
36
38
  },
37
- "engines": { "node": ">=16" },
39
+ "engines": {
40
+ "node": ">=16"
41
+ },
38
42
  "bugs": "https://github.com/audiojs/decode-caf/issues"
39
43
  }