Skip to content

Commit bdc4cc8

Browse files
committed
Improve streamable API
1 parent 364adcd commit bdc4cc8

7 files changed

Lines changed: 56 additions & 51 deletions

File tree

audio-decode.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ type Format = 'mp3' | 'flac' | 'opus' | 'oga' | 'm4a' | 'wav' | 'qoa' | 'aac' |
1717
interface FormatDecoder {
1818
/** Create a decoder instance. */
1919
(): Promise<StreamDecoder>;
20-
/** @deprecated Use decode.mp3() instead. */
21-
stream(): Promise<StreamDecoder>;
2220
}
2321

2422
/** Whole-file decode: auto-detects format. */
2523
declare function decode(buf: ArrayBuffer | Uint8Array): Promise<AudioData>;
24+
/** Chunked decode from stream or async iterable. */
25+
declare function decode(
26+
source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
27+
format: Format
28+
): AsyncGenerator<AudioData>;
2629

2730
declare namespace decode {
2831
export const mp3: FormatDecoder;
@@ -42,11 +45,8 @@ declare namespace decode {
4245

4346
export default decode;
4447

45-
/** @deprecated Use audio-decode/stream */
46-
export function decodeStream(
47-
stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
48+
/** Chunked decode from stream or async iterable. */
49+
export function decodeChunked(
50+
source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
4851
format: Format
4952
): AsyncGenerator<AudioData>;
50-
51-
/** @deprecated Use decode.mp3, decode.flac, etc. */
52-
export declare const decoders: typeof decode;

audio-decode.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/**
2-
* Audio decoder: whole-file and streaming
2+
* Audio decoder: whole-file and chunked
33
* @module audio-decode
44
*
55
* let { channelData, sampleRate } = await decode(buf)
66
*
7+
* for await (let pcm of decode(source, 'mp3')) { ... }
8+
*
79
* let dec = await decode.mp3()
810
* let { channelData, sampleRate } = await dec(chunk)
911
* await dec() // close
@@ -14,11 +16,16 @@ import getType from 'audio-type';
1416
const EMPTY = Object.freeze({ channelData: Object.freeze([]), sampleRate: 0 })
1517

1618
/**
17-
* Whole-file decode: auto-detects format
18-
* @param {ArrayBuffer|Uint8Array} src - encoded audio data
19-
* @returns {Promise<{channelData: Float32Array[], sampleRate: number}>}
19+
* Decode audio.
20+
* 1 arg: whole-file — auto-detects format, returns Promise<AudioData>
21+
* 2 args: chunked — streams from ReadableStream/AsyncIterable, returns AsyncGenerator<AudioData>
2022
*/
21-
export default async function decode(src) {
23+
export default function decode(src, format) {
24+
if (format) return decodeChunked(src, format)
25+
return decodeWhole(src)
26+
}
27+
28+
async function decodeWhole(src) {
2229
if (!src || typeof src === 'string' || !(src.buffer || src.byteLength || src.length))
2330
throw TypeError('Expected ArrayBuffer or Uint8Array')
2431
let buf = new Uint8Array(src)
@@ -36,26 +43,25 @@ export default async function decode(src) {
3643
}
3744

3845
/**
39-
* Decode a ReadableStream or async iterable of audio chunks
40-
* @param {ReadableStream|AsyncIterable} stream
46+
* Decode a ReadableStream or async iterable of encoded audio chunks.
47+
* @param {ReadableStream|AsyncIterable} source
4148
* @param {string} format - codec name
4249
* @returns {AsyncGenerator<{channelData: Float32Array[], sampleRate: number}>}
4350
*/
44-
export async function* decodeStream(stream, format) {
51+
export async function* decodeChunked(source, format) {
4552
if (!decode[format]) throw Error('No decoder for ' + format)
4653
let dec = await decode[format]()
4754
try {
48-
// Safari ReadableStream doesn't support for-await, use getReader() if available
49-
if (stream.getReader) {
50-
let reader = stream.getReader()
55+
if (source.getReader) {
56+
let reader = source.getReader()
5157
while (true) {
5258
let { done, value } = await reader.read()
5359
if (done) break
5460
let result = await dec(value instanceof Uint8Array ? value : new Uint8Array(value))
5561
if (result.channelData.length) yield result
5662
}
5763
} else {
58-
for await (let chunk of stream) {
64+
for await (let chunk of source) {
5965
let result = await dec(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk))
6066
if (result.channelData.length) yield result
6167
}
@@ -93,7 +99,6 @@ function reg(name, load) {
9399
})
94100
}
95101

96-
// TODO: remove backward compat (src arg, .stream) in next major
97102
function fmt(name, init) {
98103
let fn = async (src) => {
99104
if (!src) return init()
@@ -105,7 +110,6 @@ function fmt(name, init) {
105110
return merge(result, flushed)
106111
} catch (e) { dec.free(); throw e }
107112
}
108-
fn.stream = init
109113
return fn
110114
}
111115

@@ -128,9 +132,6 @@ reg('webm', () => import('@audio/decode-webm'))
128132
reg('amr', () => import('@audio/decode-amr'))
129133
reg('wma', () => import('@audio/decode-wma'))
130134

131-
// TODO: remove in next major
132-
export const decoders = decode
133-
134135
/**
135136
* StreamDecoder — a callable function:
136137
* dec(chunk) — decode data, returns { channelData, sampleRate }
@@ -155,7 +156,6 @@ function streamDecoder(onDecode, onFlush, onFree) {
155156
return result
156157
} catch (e) { onFree?.(); throw e }
157158
}
158-
fn.decode = fn // TODO: remove in next major
159159
fn.flush = async () => {
160160
if (done) return EMPTY
161161
return onFlush ? norm(await onFlush()) : EMPTY

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readme.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,20 @@ let b = await dec(chunk2)
4848
await dec() // close
4949
```
5050

51-
## Streaming
51+
### Streaming
5252

53-
With `ReadableStream`, `fetch`, or Node stream:
53+
Pass an async iterable source and format string — returns an async generator:
5454

5555
```js
56-
import decodeStream from 'audio-decode/stream'
56+
import decode from 'audio-decode'
5757

58-
for await (let { channelData, sampleRate } of decodeStream(response.body, 'mp3')) {
58+
for await (let { channelData, sampleRate } of decode(response.body, 'mp3')) {
5959
// process chunks
6060
}
6161
```
6262

63+
Works with `ReadableStream`, `fetch` body, Node stream, or any async iterable.
64+
6365
Formats: `mp3`, `flac`, `opus`, `oga`, `m4a`, `wav`, `qoa`, `aac`, `aiff`, `caf`, `webm`, `amr`, `wma`.
6466

6567
## See also

stream.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import type { AudioData } from './audio-decode.js';
22

33
type Format = 'mp3' | 'flac' | 'opus' | 'oga' | 'm4a' | 'wav' | 'qoa' | 'aac' | 'aiff' | 'caf' | 'webm' | 'amr' | 'wma';
44

5-
/** Decode a stream of audio chunks */
6-
declare function decodeStream(
7-
stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
5+
/** Chunked decode from stream or async iterable. */
6+
declare function decodeChunked(
7+
source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
88
format: Format
99
): AsyncGenerator<AudioData>;
1010

11+
export default decodeChunked;
12+
export { decodeChunked };
13+
1114
export default decodeStream;
1215
export { decodeStream };

stream.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { decodeStream as default, decodeStream } from './audio-decode.js'
1+
export { decodeChunked as default, decodeChunked } from './audio-decode.js'

test.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import decode, { decodeStream } from './audio-decode.js';
1+
import decode from './audio-decode.js';
22
import wav from 'audio-lena/wav';
33
import mp3 from 'audio-lena/mp3';
44
import ogg from 'audio-lena/ogg';
@@ -294,7 +294,7 @@ t('concurrent decoding', async () => {
294294
})
295295

296296
t('concurrent stream decoders', async () => {
297-
let [d1, d2] = await Promise.all([decode.mp3.stream(), decode.flac.stream()])
297+
let [d1, d2] = await Promise.all([decode.mp3(), decode.flac()])
298298
let [r1, r2] = await Promise.all([
299299
d1(new Uint8Array(mp3)),
300300
d2(new Uint8Array(flac)),
@@ -318,60 +318,60 @@ t('minimal invalid buffer', async () => {
318318
is(threw, true)
319319
})
320320

321-
// -- decodeStream --
321+
// -- chunked decode --
322322

323-
t('decodeStream mp3', async () => {
323+
t('decode mp3', async () => {
324324
let chunks = [new Uint8Array(mp3)]
325325
async function* gen() { for (let c of chunks) yield c }
326326
let total = 0
327-
for await (let r of decodeStream(gen(), 'mp3')) {
327+
for await (let r of decode(gen(), 'mp3')) {
328328
is(r.sampleRate > 0, true)
329329
total += r.channelData[0].length
330330
}
331331
is(total > 0, true, 'decoded samples')
332332
})
333333

334-
t('decodeStream ReadableStream', async () => {
334+
t('decode ReadableStream', async () => {
335335
let data = new Uint8Array(wav)
336336
let stream = new ReadableStream({
337337
start(ctrl) { ctrl.enqueue(data); ctrl.close() }
338338
})
339339
let total = 0
340-
for await (let r of decodeStream(stream, 'wav')) {
340+
for await (let r of decode(stream, 'wav')) {
341341
is(r.sampleRate, 44100)
342342
total += r.channelData[0].length
343343
}
344344
is(total > 0, true)
345345
})
346346

347-
t('decodeStream m4a', async () => {
347+
t('decode m4a', async () => {
348348
async function* gen() { yield new Uint8Array(m4a) }
349349
let total = 0
350-
for await (let r of decodeStream(gen(), 'm4a')) {
350+
for await (let r of decode(gen(), 'm4a')) {
351351
is(r.sampleRate, 44100)
352352
total += r.channelData[0].length
353353
}
354354
is(total > 0, true)
355355
})
356356

357-
t('decodeStream m4a chunked', async () => {
357+
t('decode m4a chunked', async () => {
358358
// M4A needs full file (moov atom), so chunked streaming must buffer until flush
359359
let buf = new Uint8Array(m4a), chunkSize = 16384
360360
async function* gen() {
361361
for (let off = 0; off < buf.length; off += chunkSize)
362362
yield buf.subarray(off, Math.min(off + chunkSize, buf.length))
363363
}
364364
let total = 0
365-
for await (let r of decodeStream(gen(), 'm4a')) {
365+
for await (let r of decode(gen(), 'm4a')) {
366366
total += r.channelData[0].length
367367
}
368368
let ref = await decode(m4a)
369369
is(total, ref.channelData[0].length, 'chunked M4A matches one-shot')
370370
})
371371

372-
t('decodeStream unknown format', async () => {
372+
t('decode unknown format', async () => {
373373
let threw = false
374-
try { for await (let _ of decodeStream([], 'xyz')) {} } catch { threw = true }
374+
try { for await (let _ of decode([], 'xyz')) {} } catch { threw = true }
375375
is(threw, true)
376376
})
377377

@@ -432,7 +432,7 @@ async function* chunked(buf, size = 4096) {
432432
function streamTotal(gen, fmt) {
433433
return (async () => {
434434
let total = 0, sr = 0
435-
for await (let r of decodeStream(gen, fmt)) {
435+
for await (let r of decode(gen, fmt)) {
436436
sr = r.sampleRate; total += r.channelData[0].length
437437
}
438438
return { total, sr }
@@ -533,6 +533,6 @@ t('chunked stream tiny chunks wav', async () => {
533533
t('chunked stream yields multiple results', async () => {
534534
// verify streaming actually yields multiple chunks (not one big blob)
535535
let count = 0
536-
for await (let r of decodeStream(chunked(wav, 4096), 'wav')) count++
536+
for await (let r of decode(chunked(wav, 4096), 'wav')) count++
537537
is(count > 1, true, 'multiple yields from stream')
538538
})

0 commit comments

Comments
 (0)