@audio/webm-decode 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 audiojs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # webm-decode
2
+
3
+ Decode WebM audio (Opus and Vorbis) to PCM float samples. EBML demuxer in pure JS, codec decoding via WASM.
4
+
5
+ Part of [audio-decode](https://github.com/audiojs/audio-decode).
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm i @audio/webm-decode
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ import decode from '@audio/webm-decode'
17
+
18
+ let { channelData, sampleRate } = await decode(webmBuffer)
19
+ ```
20
+
21
+ ### Streaming
22
+
23
+ ```js
24
+ import { decoder } from '@audio/webm-decode'
25
+
26
+ let dec = await decoder()
27
+ let result = await dec.decode(chunk)
28
+ let flushed = await dec.flush()
29
+ dec.free()
30
+ ```
31
+
32
+ ## API
33
+
34
+ ### `decode(src): Promise<AudioData>`
35
+
36
+ Whole-file decode. Accepts `Uint8Array` or `ArrayBuffer`.
37
+
38
+ ### `decoder(): Promise<WebmDecoder>`
39
+
40
+ Creates a decoder instance.
41
+
42
+ - **`dec.decode(data)`** — decode chunk, returns `Promise<AudioData>`
43
+ - **`dec.flush()`** — flush remaining data
44
+ - **`dec.free()`** — release resources
45
+
46
+ Note: `decode()` and `flush()` are async (unlike the sync PCM decoders) because Opus decoding happens in WASM.
47
+
48
+ ## Codecs
49
+
50
+ - **Opus** — via [opus-decoder](https://github.com/eshaz/wasm-audio-decoders)
51
+ - **Vorbis** — via [@wasm-audio-decoders/ogg-vorbis](https://github.com/eshaz/wasm-audio-decoders) (raw frames wrapped in OGG pages)
52
+
53
+ ## License
54
+
55
+ MIT
56
+
57
+ <a href="https://github.com/krishnized/license/">ॐ</a>
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@audio/webm-decode",
3
+ "version": "1.0.0",
4
+ "description": "Decode WebM audio (Opus, Vorbis) to PCM samples",
5
+ "type": "module",
6
+ "main": "webm-decode.js",
7
+ "types": "webm-decode.d.ts",
8
+ "exports": {
9
+ ".": "./webm-decode.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "webm-decode.js",
14
+ "webm-decode.d.ts",
15
+ "LICENSE"
16
+ ],
17
+ "dependencies": {
18
+ "@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
19
+ "opus-decoder": "^0.7.6"
20
+ },
21
+ "keywords": [
22
+ "webm",
23
+ "matroska",
24
+ "ebml",
25
+ "opus",
26
+ "vorbis",
27
+ "audio",
28
+ "decode",
29
+ "decoder",
30
+ "pcm"
31
+ ],
32
+ "publishConfig": { "access": "public" },
33
+ "license": "MIT",
34
+ "author": "audiojs",
35
+ "homepage": "https://github.com/audiojs/webm-decode#readme",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/audiojs/webm-decode.git"
39
+ },
40
+ "engines": {
41
+ "node": ">=16"
42
+ },
43
+ "bugs": "https://github.com/audiojs/webm-decode/issues",
44
+ "scripts": {
45
+ "test": "node test.js"
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ interface AudioData {
2
+ channelData: Float32Array[];
3
+ sampleRate: number;
4
+ }
5
+
6
+ interface WebmDecoder {
7
+ decode(data: Uint8Array): Promise<AudioData>;
8
+ flush(): Promise<AudioData>;
9
+ free(): void;
10
+ }
11
+
12
+ /** Decode WebM audio buffer (Opus, Vorbis) to PCM samples */
13
+ export default function decode(src: ArrayBuffer | Uint8Array): Promise<AudioData>;
14
+
15
+ /** Create streaming decoder instance */
16
+ export function decoder(): Promise<WebmDecoder>;
package/webm-decode.js ADDED
@@ -0,0 +1,452 @@
1
+ /**
2
+ * WebM audio decoder — demuxes EBML, decodes Opus/Vorbis to PCM
3
+ *
4
+ * let { channelData, sampleRate } = await decode(webmbuf)
5
+ * let dec = await decoder(); let result = await dec.decode(chunk)
6
+ */
7
+
8
+ const EMPTY = Object.freeze({ channelData: [], sampleRate: 0 })
9
+
10
+ // EBML element IDs
11
+ const ID_EBML = 0x1A45DFA3
12
+ const ID_SEGMENT = 0x18538067
13
+ const ID_TRACKS = 0x1654AE6B
14
+ const ID_TRACK_ENTRY = 0xAE
15
+ const ID_TRACK_NUMBER = 0xD7
16
+ const ID_TRACK_TYPE = 0x83
17
+ const ID_CODEC_ID = 0x86
18
+ const ID_CODEC_PRIVATE = 0x63A2
19
+ const ID_AUDIO = 0xE1
20
+ const ID_SAMPLE_RATE = 0xB5
21
+ const ID_CHANNELS = 0x9F
22
+ const ID_CODEC_DELAY = 0x56AA
23
+ const ID_SEEK_PRE_ROLL = 0x56BB
24
+ const ID_CLUSTER = 0x1F43B675
25
+ const ID_SIMPLE_BLOCK = 0xA3
26
+ const ID_BLOCK_GROUP = 0xA0
27
+ const ID_BLOCK = 0xA1
28
+
29
+ // Master elements whose children we descend into
30
+ const MASTER = new Set([
31
+ ID_EBML, ID_SEGMENT, ID_TRACKS, ID_AUDIO,
32
+ ID_CLUSTER, ID_BLOCK_GROUP
33
+ ])
34
+
35
+ // Unknown-size sentinel values per VINT length (all value bits = 1)
36
+ const UNKNOWN_SIZE = [0x7F, 0x3FFF, 0x1FFFFF, 0x0FFFFFFF, 0x07FFFFFFFF, 0x03FFFFFFFFFF, 0x01FFFFFFFFFFFF, 0x00FFFFFFFFFFFFFF]
37
+
38
+ /**
39
+ * Read EBML element ID (VINT with leading 1 retained)
40
+ */
41
+ function readId(b, o) {
42
+ if (o >= b.length) return null
43
+ let first = b[o], len = 1, mask = 0x80
44
+ while (len <= 4 && !(first & mask)) { len++; mask >>= 1 }
45
+ if (len > 4) return null
46
+ let val = first
47
+ for (let i = 1; i < len; i++) {
48
+ if (o + i >= b.length) return null
49
+ val = val * 256 + b[o + i]
50
+ }
51
+ return { val, len }
52
+ }
53
+
54
+ /**
55
+ * Read EBML VINT data size (leading 1 masked off)
56
+ * Returns -1 for unknown size
57
+ */
58
+ function readSize(b, o) {
59
+ if (o >= b.length) return null
60
+ let first = b[o], len = 1, mask = 0x80
61
+ while (len <= 8 && !(first & mask)) { len++; mask >>= 1 }
62
+ if (len > 8) return null
63
+ let val = first & (mask - 1)
64
+ for (let i = 1; i < len; i++) {
65
+ if (o + i >= b.length) return null
66
+ val = val * 256 + b[o + i]
67
+ }
68
+ if (val === UNKNOWN_SIZE[len - 1]) return { val: -1, len }
69
+ return { val, len }
70
+ }
71
+
72
+ function readUint(b, o, n) {
73
+ let v = 0
74
+ for (let i = 0; i < n; i++) v = v * 256 + b[o + i]
75
+ return v
76
+ }
77
+
78
+ function readFloat(b, o, n) {
79
+ let dv = new DataView(b.buffer, b.byteOffset, b.byteLength)
80
+ if (n === 4) return dv.getFloat32(o)
81
+ if (n === 8) return dv.getFloat64(o)
82
+ return 0
83
+ }
84
+
85
+ function readStr(b, o, n) {
86
+ let s = ''
87
+ for (let i = 0; i < n; i++) {
88
+ if (b[o + i] === 0) break
89
+ s += String.fromCharCode(b[o + i])
90
+ }
91
+ return s
92
+ }
93
+
94
+ /**
95
+ * Parse Opus identification header (CodecPrivate in WebM)
96
+ * RFC 7845: "OpusHead" + version + channels + preSkip(LE16) + sampleRate(LE32) + outputGain(LE16) + mappingFamily...
97
+ */
98
+ function parseOpusHead(d) {
99
+ if (!d || d.length < 19) return null
100
+ if (readStr(d, 0, 8) !== 'OpusHead') return null
101
+ if (d[8] > 15) return null // unknown major version
102
+
103
+ let channels = d[9]
104
+ let preSkip = d[10] | (d[11] << 8)
105
+ let sampleRate = d[12] | (d[13] << 8) | (d[14] << 16) | (d[15] << 24)
106
+ let mappingFamily = d[18]
107
+ let streamCount = 1, coupledStreamCount = channels > 1 ? 1 : 0
108
+ let channelMappingTable = channels === 1 ? [0] : [0, 1]
109
+
110
+ if (mappingFamily > 0 && d.length >= 21 + channels) {
111
+ streamCount = d[19]
112
+ coupledStreamCount = d[20]
113
+ channelMappingTable = Array.from(d.subarray(21, 21 + channels))
114
+ }
115
+
116
+ return { channels, preSkip, sampleRate, mappingFamily, streamCount, coupledStreamCount, channelMappingTable }
117
+ }
118
+
119
+ /**
120
+ * Parse WebM EBML structure, extract first audio track info + raw codec frames
121
+ */
122
+ function parseWebm(buf) {
123
+ let b = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
124
+ if (b.length < 4) throw Error('Not a WebM file')
125
+
126
+ let id = readId(b, 0)
127
+ if (!id || id.val !== ID_EBML) throw Error('Not a WebM file')
128
+
129
+ // Collect track entries, then pick the first audio track
130
+ let entries = [] // each: { number, type, codec, sampleRate, channels, codecPrivate, codecDelay, seekPreRoll }
131
+ let curEntry = null // current TrackEntry being parsed
132
+ let audioTrack = null
133
+ let frames = []
134
+
135
+ function walk(start, end) {
136
+ let pos = start
137
+ while (pos < end) {
138
+ let eid = readId(b, pos)
139
+ if (!eid) break
140
+ let siz = readSize(b, pos + eid.len)
141
+ if (!siz) break
142
+
143
+ let dataOff = pos + eid.len + siz.len
144
+ let dataLen = siz.val
145
+ let elemEnd = dataLen < 0 ? end : dataOff + dataLen
146
+ if (elemEnd > end) elemEnd = end
147
+ if (dataOff > end) break
148
+
149
+ let elemId = eid.val
150
+
151
+ if (elemId === ID_TRACK_ENTRY) {
152
+ // Start a new track entry, then descend
153
+ curEntry = { number: 0, type: 0, codec: '', sampleRate: 48000, channels: 2, codecPrivate: null, codecDelay: 0, seekPreRoll: 0 }
154
+ walk(dataOff, elemEnd)
155
+ entries.push(curEntry)
156
+ // Pick first audio track
157
+ if (!audioTrack && curEntry.type === 2 && curEntry.codec) audioTrack = curEntry
158
+ curEntry = null
159
+ } else if (MASTER.has(elemId)) {
160
+ walk(dataOff, elemEnd)
161
+ } else if (curEntry) {
162
+ // Inside a TrackEntry
163
+ if (elemId === ID_TRACK_NUMBER) curEntry.number = readUint(b, dataOff, dataLen)
164
+ else if (elemId === ID_TRACK_TYPE) curEntry.type = readUint(b, dataOff, dataLen)
165
+ else if (elemId === ID_CODEC_ID) curEntry.codec = readStr(b, dataOff, dataLen)
166
+ else if (elemId === ID_CODEC_PRIVATE) curEntry.codecPrivate = b.slice(dataOff, dataOff + dataLen)
167
+ else if (elemId === ID_SAMPLE_RATE && dataLen > 0) curEntry.sampleRate = readFloat(b, dataOff, dataLen)
168
+ else if (elemId === ID_CHANNELS && dataLen > 0) curEntry.channels = readUint(b, dataOff, dataLen)
169
+ else if (elemId === ID_CODEC_DELAY && dataLen > 0) curEntry.codecDelay = readUint(b, dataOff, dataLen)
170
+ else if (elemId === ID_SEEK_PRE_ROLL && dataLen > 0) curEntry.seekPreRoll = readUint(b, dataOff, dataLen)
171
+ } else if ((elemId === ID_SIMPLE_BLOCK || elemId === ID_BLOCK) && audioTrack && dataLen > 0) {
172
+ let bp = dataOff
173
+ let tn = readSize(b, bp)
174
+ if (tn && tn.val === audioTrack.number) {
175
+ bp += tn.len + 3 // skip track VINT + 2 bytes timestamp + 1 byte flags
176
+ if (bp < dataOff + dataLen) {
177
+ frames.push(b.subarray(bp, dataOff + dataLen))
178
+ }
179
+ }
180
+ }
181
+
182
+ pos = elemEnd
183
+ }
184
+ }
185
+
186
+ walk(0, b.length)
187
+
188
+ if (!audioTrack) throw Error('No audio track found in WebM')
189
+
190
+ return {
191
+ codec: audioTrack.codec,
192
+ sampleRate: audioTrack.sampleRate,
193
+ channels: audioTrack.channels,
194
+ codecPrivate: audioTrack.codecPrivate,
195
+ codecDelay: audioTrack.codecDelay,
196
+ seekPreRoll: audioTrack.seekPreRoll,
197
+ frames
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Decode raw Opus frames via opus-decoder
203
+ */
204
+ async function decodeOpus(info) {
205
+ let { OpusDecoder } = await import('opus-decoder')
206
+
207
+ let head = info.codecPrivate ? parseOpusHead(info.codecPrivate) : null
208
+ let channels = head?.channels || info.channels || 2
209
+ let preSkip = head?.preSkip || 0
210
+
211
+ // CodecDelay in WebM is nanoseconds; convert to samples as fallback
212
+ if (!preSkip && info.codecDelay) preSkip = Math.round(info.codecDelay / 1e9 * 48000)
213
+
214
+ let opts = { channels, sampleRate: 48000, preSkip }
215
+
216
+ if (head && head.mappingFamily > 0) {
217
+ opts.streamCount = head.streamCount
218
+ opts.coupledStreamCount = head.coupledStreamCount
219
+ opts.channelMappingTable = head.channelMappingTable
220
+ } else if (channels === 1) {
221
+ opts.streamCount = 1
222
+ opts.coupledStreamCount = 0
223
+ opts.channelMappingTable = [0]
224
+ } else if (channels === 2) {
225
+ opts.streamCount = 1
226
+ opts.coupledStreamCount = 1
227
+ opts.channelMappingTable = [0, 1]
228
+ }
229
+
230
+ let dec = new OpusDecoder(opts)
231
+ await dec.ready
232
+
233
+ if (!info.frames.length) { dec.free(); return EMPTY }
234
+
235
+ let result = dec.decodeFrames(info.frames)
236
+ dec.free()
237
+
238
+ if (!result?.channelData?.length) return EMPTY
239
+
240
+ let { channelData, samplesDecoded, sampleRate } = result
241
+ if (samplesDecoded != null && samplesDecoded < channelData[0].length)
242
+ channelData = channelData.map(ch => ch.subarray(0, samplesDecoded))
243
+
244
+ return { channelData, sampleRate }
245
+ }
246
+
247
+ /**
248
+ * Parse Matroska Vorbis CodecPrivate into 3 header packets.
249
+ * Format: byte 0 = num_packets-1 (2), then Xiph lacing sizes, then concatenated packets.
250
+ */
251
+ function parseVorbisPrivate(d) {
252
+ if (!d || d.length < 3 || d[0] !== 2) return null
253
+ let pos = 1, sizes = []
254
+ for (let i = 0; i < 2; i++) {
255
+ let sz = 0
256
+ while (pos < d.length && d[pos] === 255) { sz += 255; pos++ }
257
+ if (pos < d.length) { sz += d[pos]; pos++ }
258
+ sizes.push(sz)
259
+ }
260
+ let h1 = d.slice(pos, pos + sizes[0])
261
+ let h2 = d.slice(pos + sizes[0], pos + sizes[0] + sizes[1])
262
+ let h3 = d.slice(pos + sizes[0] + sizes[1])
263
+ if (h1[0] !== 1 || h2[0] !== 3 || h3[0] !== 5) return null
264
+ return [h1, h2, h3]
265
+ }
266
+
267
+ // OGG CRC lookup table (polynomial 0x04C11DB7)
268
+ const OGG_CRC = new Uint32Array(256)
269
+ for (let i = 0; i < 256; i++) {
270
+ let r = i << 24
271
+ for (let j = 0; j < 8; j++) r = (r << 1) ^ ((r >>> 31) * 0x04C11DB7)
272
+ OGG_CRC[i] = r >>> 0
273
+ }
274
+ function oggCrc(buf) {
275
+ let c = 0
276
+ for (let i = 0; i < buf.length; i++) c = ((c << 8) ^ OGG_CRC[((c >>> 24) ^ buf[i]) & 0xFF]) >>> 0
277
+ return c
278
+ }
279
+
280
+ /**
281
+ * Build a single OGG page from complete packets.
282
+ */
283
+ function makeOggPage(packets, granule, serial, seq, flags) {
284
+ // Build segment table: each packet uses ceil(len/255) segments for 255-byte chunks + 1 terminating segment
285
+ let segs = []
286
+ for (let p of packets) {
287
+ let len = p.length
288
+ while (len >= 255) { segs.push(255); len -= 255 }
289
+ segs.push(len) // terminating segment (0 if packet is exact multiple of 255)
290
+ }
291
+
292
+ let bodyLen = 0
293
+ for (let p of packets) bodyLen += p.length
294
+ let headerLen = 27 + segs.length
295
+ let page = new Uint8Array(headerLen + bodyLen)
296
+ let dv = new DataView(page.buffer)
297
+
298
+ page[0] = 0x4F; page[1] = 0x67; page[2] = 0x67; page[3] = 0x53 // "OggS"
299
+ page[4] = 0 // version
300
+ page[5] = flags
301
+ // Granule position (64-bit LE); -1 = not set (0xFFFFFFFFFFFFFFFF)
302
+ if (granule < 0) { dv.setUint32(6, 0xFFFFFFFF, true); dv.setUint32(10, 0xFFFFFFFF, true) }
303
+ else { dv.setUint32(6, granule >>> 0, true); dv.setUint32(10, (granule / 0x100000000) >>> 0, true) }
304
+ dv.setUint32(14, serial, true)
305
+ dv.setUint32(18, seq, true)
306
+ dv.setUint32(22, 0, true) // CRC placeholder
307
+ page[26] = segs.length
308
+ for (let i = 0; i < segs.length; i++) page[27 + i] = segs[i]
309
+ let off = headerLen
310
+ for (let p of packets) { page.set(p, off); off += p.length }
311
+ dv.setUint32(22, oggCrc(page), true)
312
+
313
+ return page
314
+ }
315
+
316
+ /**
317
+ * Wrap raw Vorbis header packets and audio frames into an OGG bitstream.
318
+ * Max 255 segments per OGG page. Granule on EOS page set high to avoid truncation
319
+ * (exact sample count is unknown without deep Vorbis mode parsing).
320
+ */
321
+ function vorbisToOgg(headers, frames) {
322
+ let serial = 0x564F5242, pages = [], seq = 0 // "VORB"
323
+
324
+ // Page 0: BOS — identification header only, granule 0
325
+ pages.push(makeOggPage([headers[0]], 0, serial, seq++, 0x02))
326
+ // Page 1: comment + setup headers, granule 0
327
+ pages.push(makeOggPage([headers[1], headers[2]], 0, serial, seq++, 0))
328
+
329
+ // Audio pages — pack frames respecting 255-segment limit
330
+ let i = 0
331
+ while (i < frames.length) {
332
+ let pkt = [], segCount = 0
333
+ while (i < frames.length) {
334
+ let needed = Math.floor(frames[i].length / 255) + 1
335
+ if (segCount + needed > 255) break
336
+ pkt.push(frames[i])
337
+ segCount += needed
338
+ i++
339
+ }
340
+ let isLast = i >= frames.length
341
+ // Granule: -1 (not set) on intermediate pages; max safe int on EOS to avoid truncation
342
+ pages.push(makeOggPage(pkt, isLast ? 0x1FFFFFFFFFFFFF : -1, serial, seq++, isLast ? 0x04 : 0))
343
+ }
344
+
345
+ let totalLen = 0
346
+ for (let p of pages) totalLen += p.length
347
+ let ogg = new Uint8Array(totalLen)
348
+ let off = 0
349
+ for (let p of pages) { ogg.set(p, off); off += p.length }
350
+ return ogg
351
+ }
352
+
353
+ /**
354
+ * Decode raw Vorbis frames via ogg-vorbis decoder
355
+ */
356
+ async function decodeVorbis(info) {
357
+ let { OggVorbisDecoder } = await import('@wasm-audio-decoders/ogg-vorbis')
358
+
359
+ let headers = parseVorbisPrivate(info.codecPrivate)
360
+ if (!headers) throw Error('Invalid Vorbis CodecPrivate')
361
+
362
+ if (!info.frames.length) return EMPTY
363
+
364
+ let ogg = vorbisToOgg(headers, info.frames)
365
+ let dec = new OggVorbisDecoder()
366
+ await dec.ready
367
+
368
+ let result = await dec.decodeFile(ogg)
369
+ dec.free()
370
+
371
+ if (!result?.channelData?.length) return EMPTY
372
+
373
+ let { channelData, samplesDecoded, sampleRate } = result
374
+ if (samplesDecoded != null && samplesDecoded < channelData[0].length)
375
+ channelData = channelData.map(ch => ch.subarray(0, samplesDecoded))
376
+
377
+ return { channelData, sampleRate }
378
+ }
379
+
380
+ /**
381
+ * Whole-file decode
382
+ * @param {Uint8Array|ArrayBuffer} src
383
+ * @returns {Promise<{channelData: Float32Array[], sampleRate: number}>}
384
+ */
385
+ export default async function decode(src) {
386
+ if (!src || typeof src === 'string' || !(src.buffer || src.byteLength != null || src.length))
387
+ throw TypeError('Expected ArrayBuffer or Uint8Array')
388
+ let buf = src instanceof Uint8Array ? src : new Uint8Array(src.buffer || src)
389
+ if (!buf.length) throw Error('Not a WebM file')
390
+ let dec = await decoder()
391
+ try {
392
+ let result = await dec.decode(buf)
393
+ let flushed = await dec.flush()
394
+ return merge(result, flushed)
395
+ } finally {
396
+ dec.free()
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Create streaming decoder instance
402
+ * @returns {Promise<{decode(chunk: Uint8Array): Promise<AudioData>, flush(): Promise<AudioData>, free(): void}>}
403
+ */
404
+ export async function decoder() {
405
+ let freed = false, chunks = [], totalLen = 0
406
+
407
+ return {
408
+ async decode(data) {
409
+ if (freed) throw Error('Decoder already freed')
410
+ if (!data?.length) return EMPTY
411
+ chunks.push(data instanceof Uint8Array ? data : new Uint8Array(data))
412
+ totalLen += data.length
413
+
414
+ let buf = chunks.length === 1 ? chunks[0] : concat(chunks, totalLen)
415
+ let info = parseWebm(buf)
416
+ if (!info.frames.length) return EMPTY
417
+
418
+ if (info.codec === 'A_OPUS') return decodeOpus(info)
419
+ if (info.codec === 'A_VORBIS') return decodeVorbis(info)
420
+ throw Error('Unsupported WebM codec: ' + info.codec)
421
+ },
422
+ async flush() {
423
+ if (freed) return EMPTY
424
+ freed = true
425
+ chunks = []; totalLen = 0
426
+ return EMPTY
427
+ },
428
+ free() {
429
+ freed = true
430
+ chunks = []; totalLen = 0
431
+ }
432
+ }
433
+ }
434
+
435
+ function concat(parts, totalLen) {
436
+ let buf = new Uint8Array(totalLen), off = 0
437
+ for (let c of parts) { buf.set(c, off); off += c.length }
438
+ return buf
439
+ }
440
+
441
+ function merge(a, b) {
442
+ if (!b?.channelData?.length) return a
443
+ if (!a?.channelData?.length) return b
444
+ return {
445
+ channelData: a.channelData.map((ch, i) => {
446
+ let m = new Float32Array(ch.length + b.channelData[i].length)
447
+ m.set(ch); m.set(b.channelData[i], ch.length)
448
+ return m
449
+ }),
450
+ sampleRate: a.sampleRate
451
+ }
452
+ }