Skip to content

Commit feaa262

Browse files
committed
Simplify API
1 parent 50c8095 commit feaa262

4 files changed

Lines changed: 85 additions & 65 deletions

File tree

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,18 @@ const opus = await encode.opus(channelData, { sampleRate: 48000, bitrate: 96 });
4040

4141
### Stream encoding
4242

43-
For chunk-by-chunk encoding, use `.stream()`:
43+
Call with just options (no data) to create a streaming encoder:
4444

4545
```js
4646
import encode from 'encode-audio';
4747

48-
const encoder = await encode.mp3.stream({ sampleRate: 44100, bitrate: 128 });
48+
const enc = await encode.mp3({ sampleRate: 44100, bitrate: 128 });
4949

50-
const a = await encoder.encode(chunk1); // Uint8Array
51-
const b = await encoder.encode(chunk2);
52-
const c = await encoder.encode(); // end of stream — flush + free
50+
const a = await enc(chunk1); // Uint8Array
51+
const b = await enc(chunk2);
52+
const c = await enc(); // end of stream — flush + free
5353

54-
// explicit methods
55-
// encoder.flush(), encoder.free()
54+
// explicit control: enc.flush(), enc.free()
5655
```
5756

5857
### Options

audio-encode.d.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
export interface StreamEncoder {
2-
/** Encode a chunk of audio. */
3-
encode(channelData: Float32Array[] | Float32Array): Promise<Uint8Array>;
4-
/** Flush remaining data, finalize, and free resources. */
5-
encode(): Promise<Uint8Array>;
6-
/** Flush without freeing. */
7-
flush(): Promise<Uint8Array>;
8-
/** Free resources without flushing. */
9-
free(): void;
10-
}
1+
type AudioInput = Float32Array[] | Float32Array | { numberOfChannels: number; getChannelData(i: number): Float32Array };
112

123
export interface EncodeOptions {
134
/** Output sample rate (required). */
@@ -27,8 +18,25 @@ export interface EncodeOptions {
2718
[key: string]: any;
2819
}
2920

21+
export interface StreamEncoder {
22+
/** Encode a chunk of audio. */
23+
(channelData: AudioInput): Promise<Uint8Array>;
24+
/** Flush remaining data, finalize, and free resources. */
25+
(): Promise<Uint8Array>;
26+
/** @deprecated Use enc() instead. */
27+
encode(channelData?: AudioInput): Promise<Uint8Array>;
28+
/** Flush without freeing. */
29+
flush(): Promise<Uint8Array>;
30+
/** Free resources without flushing. */
31+
free(): void;
32+
}
33+
3034
export interface FormatEncoder {
31-
(channelData: Float32Array[] | Float32Array, opts: EncodeOptions): Promise<Uint8Array>;
35+
/** Whole-file encode. */
36+
(channelData: AudioInput, opts: EncodeOptions): Promise<Uint8Array>;
37+
/** Create streaming encoder. */
38+
(opts: EncodeOptions): Promise<StreamEncoder>;
39+
/** @deprecated Use encode.fmt(opts) instead. */
3240
stream(opts: EncodeOptions): Promise<StreamEncoder>;
3341
}
3442

audio-encode.js

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
*
55
* let buf = await encode.wav(channelData, { sampleRate: 44100 })
66
*
7-
* let enc = await encode.mp3.stream({ sampleRate: 44100, bitrate: 128 })
8-
* let chunk = enc.encode(channelData)
9-
* let final = enc.encode() // flush + free
7+
* let enc = await encode.mp3({ sampleRate: 44100, bitrate: 128 })
8+
* let chunk = await enc(channelData)
9+
* let final = await enc() // flush + free
1010
*/
1111

1212
const EMPTY = new Uint8Array(0)
@@ -32,21 +32,26 @@ reg('flac', () => import('@audio/flac-encode'))
3232
reg('opus', () => import('@audio/opus-encode'))
3333

3434
/**
35-
* Wrap a stream factory into whole-file encoder + .stream
36-
* @param {function} init - async (opts) => StreamEncoder
35+
* Wrap a stream factory into whole-file encoder + streaming
36+
* 1 arg (opts) → streaming encoder function
37+
* 2 args (data, opts) → whole-file encode
3738
*/
3839
function fmt(init) {
39-
let fn = async (data, opts = {}) => {
40+
let fn = async (data, opts) => {
41+
// 1 arg = streaming: encode.mp3({ sampleRate })
42+
if (!opts) return init(data)
43+
// 2 args = whole-file: encode.mp3(channelData, { sampleRate })
4044
if (!opts.sampleRate) throw Error('sampleRate is required')
4145
let ch = channels(data)
4246
if (!ch.length || !ch[0].length) return EMPTY
4347
let enc = await init(opts)
4448
try {
45-
let result = await enc.encode(ch)
46-
let flushed = await enc.encode()
49+
let result = await enc(ch)
50+
let flushed = await enc()
4751
return merge(result, flushed)
4852
} catch (e) { enc.free(); throw e }
4953
}
54+
// TODO: remove .stream in next major
5055
fn.stream = init
5156
return fn
5257
}
@@ -69,41 +74,42 @@ function channels(data) {
6974
}
7075

7176
/**
72-
* StreamEncoder:
73-
* .encode(channelData) — encode audio, returns Uint8Array
74-
* .encode() — flush + finalize + free
75-
* .flush() — flush without freeing
76-
* .free() — release without flushing
77+
* StreamEncoder — a callable function:
78+
* enc(channelData) — encode audio, returns Uint8Array
79+
* enc() — flush + finalize + free
80+
* enc.flush() — flush without freeing
81+
* enc.free() — release without flushing
7782
*/
7883
export function streamEncoder(onEncode, onFlush, onFree) {
7984
let done = false
80-
return {
81-
async encode(data) {
82-
if (data) {
83-
if (done) throw Error('Encoder already freed')
84-
let ch = channels(data)
85-
try { return norm(await onEncode(ch)) }
86-
catch (e) { done = true; onFree?.(); throw e }
87-
}
88-
// no args = end of stream
89-
if (done) return EMPTY
90-
done = true
91-
try {
92-
let result = onFlush ? norm(await onFlush()) : EMPTY
93-
onFree?.()
94-
return result
95-
} catch (e) { onFree?.(); throw e }
96-
},
97-
async flush() {
98-
if (done) return EMPTY
99-
return onFlush ? norm(await onFlush()) : EMPTY
100-
},
101-
free() {
102-
if (done) return
103-
done = true
104-
onFree?.()
85+
let fn = async (data) => {
86+
if (data) {
87+
if (done) throw Error('Encoder already freed')
88+
let ch = channels(data)
89+
try { return norm(await onEncode(ch)) }
90+
catch (e) { done = true; onFree?.(); throw e }
10591
}
92+
// no args = end of stream
93+
if (done) return EMPTY
94+
done = true
95+
try {
96+
let result = onFlush ? norm(await onFlush()) : EMPTY
97+
onFree?.()
98+
return result
99+
} catch (e) { onFree?.(); throw e }
100+
}
101+
// TODO: remove .encode in next major
102+
fn.encode = fn
103+
fn.flush = async () => {
104+
if (done) return EMPTY
105+
return onFlush ? norm(await onFlush()) : EMPTY
106106
}
107+
fn.free = () => {
108+
if (done) return
109+
done = true
110+
onFree?.()
111+
}
112+
return fn
107113
}
108114

109115
// ensure Uint8Array
@@ -123,4 +129,3 @@ function merge(a, b) {
123129
out.set(b, a.length)
124130
return out
125131
}
126-

test.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async function getLena() {
2323

2424
// --- format round-trip tests with lena ---
2525

26-
t('wav round-trip (lena)', async () => {
26+
t('wav round-trip', async () => {
2727
let { channelData, sampleRate } = await getLena()
2828
let buf = await encode.wav(channelData, { sampleRate })
2929
ok(buf.length > 44, 'has data')
@@ -33,7 +33,7 @@ t('wav round-trip (lena)', async () => {
3333
almost(rms(dec.channelData[0]), rms(channelData[0]), 0.001, 'rms matches')
3434
})
3535

36-
t('aiff encode (lena)', async () => {
36+
t('aiff encode', async () => {
3737
let { channelData, sampleRate } = await getLena()
3838
let buf = await encode.aiff(channelData, { sampleRate })
3939
ok(buf.length > 54, 'has data')
@@ -43,7 +43,7 @@ t('aiff encode (lena)', async () => {
4343
is(dv.getInt16(20, false), 1, 'mono')
4444
})
4545

46-
t('mp3 round-trip (lena)', async () => {
46+
t('mp3 round-trip', async () => {
4747
let { channelData, sampleRate } = await getLena()
4848
let buf = await encode.mp3(channelData, { sampleRate, channels: 1, bitrate: 128 })
4949
ok(buf.length > 0)
@@ -52,7 +52,7 @@ t('mp3 round-trip (lena)', async () => {
5252
almost(rms(dec.channelData[0]), rms(channelData[0]), 0.05, 'rms within lossy tolerance')
5353
})
5454

55-
t('ogg round-trip (lena)', async () => {
55+
t('ogg round-trip', async () => {
5656
let { channelData, sampleRate } = await getLena()
5757
let buf = await encode.ogg(channelData, { sampleRate, channels: 1, quality: 5 })
5858
ok(buf.length > 0)
@@ -61,7 +61,7 @@ t('ogg round-trip (lena)', async () => {
6161
almost(rms(dec.channelData[0]), rms(channelData[0]), 0.05, 'rms within lossy tolerance')
6262
})
6363

64-
t('flac round-trip (lena)', async () => {
64+
t('flac round-trip', async () => {
6565
let { channelData, sampleRate } = await getLena()
6666
let buf = await encode.flac(channelData, { sampleRate })
6767
ok(buf.length > 0)
@@ -71,16 +71,24 @@ t('flac round-trip (lena)', async () => {
7171
almost(rms(dec.channelData[0]), rms(channelData[0]), 0.001, 'rms near-identical (lossless)')
7272
})
7373

74-
t('opus round-trip (lena)', async () => {
74+
t('opus round-trip', async () => {
7575
let { channelData, sampleRate } = await getLena()
7676
let buf = await encode.opus(channelData, { sampleRate, channels: 1, bitrate: 96 })
7777
ok(buf.length > 0)
7878
let dec = await decode(buf)
79-
is(dec.sampleRate, 48000) // opus always decodes at 48kHz
79+
is(dec.sampleRate, 48000)
8080
almost(rms(dec.channelData[0]), rms(channelData[0]), 0.05, 'rms within lossy tolerance')
8181
})
8282

83-
t('wav streaming', async () => {
83+
t('streaming (callable)', async () => {
84+
let enc = await encode.wav({ sampleRate: 44100 })
85+
let c1 = await enc(sine(44100, 440, 0.5))
86+
let c2 = await enc(sine(44100, 440, 0.5))
87+
let final = await enc()
88+
ok(c1.length > 0 || c2.length > 0 || final.length > 0)
89+
})
90+
91+
t('streaming (deprecated .stream/.encode)', async () => {
8492
let enc = await encode.wav.stream({ sampleRate: 44100 })
8593
let c1 = await enc.encode(sine(44100, 440, 0.5))
8694
let c2 = await enc.encode(sine(44100, 440, 0.5))

0 commit comments

Comments
 (0)