decibri 4.1.0 → 4.3.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/CHANGELOG.md +12 -0
- package/MIGRATION.md +14 -0
- package/README.md +46 -0
- package/examples/README.md +79 -0
- package/examples/browser-speaker-test.html +159 -0
- package/examples/decibri.browser.js +594 -0
- package/index.js +52 -52
- package/package.json +5 -5
- package/src/browser/decibri-browser.js +1 -1
- package/src/browser/decibri-output-browser.js +377 -0
- package/src/browser/index.d.ts +85 -0
- package/src/browser/index.js +2 -1
- package/src/browser/output-worklet-inline.js +16 -0
- package/src/browser/output-worklet-processor.js +168 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,18 @@ For other decibri packages, see:
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [4.3.0] - Unreleased
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- The Silero VAD never detected speech due to a missing 64-sample audio context required by Silero v5. VAD probabilities now reflect real speech activity. If you consume VAD output, expect meaningful probabilities where previously everything sat near zero.
|
|
19
|
+
|
|
20
|
+
## [4.2.0] - 2026-05-31
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Browser `Speaker` for audio playback through the Web Audio API: `start()`, async `write(chunk)`, async `drain()`, `stop()`, and an `isPlaying` getter, with `int16` and `float32` input and resampling from the source rate to the output rate. Playback is started from a user gesture, as browsers require. This adds playback to the browser build alongside the existing browser `Microphone` capture. The release is browser-only and additive: the Node.js API and behavior are unchanged.
|
|
25
|
+
|
|
14
26
|
## [4.1.0] - 2026-05-31
|
|
15
27
|
|
|
16
28
|
### Added
|
package/MIGRATION.md
CHANGED
|
@@ -5,6 +5,20 @@ vocabulary that matches the Rust and Python packages, and tidies several option
|
|
|
5
5
|
and return shapes. This guide lists every breaking change with before and after
|
|
6
6
|
code.
|
|
7
7
|
|
|
8
|
+
## New in 4.2.0 (additive, nothing to migrate)
|
|
9
|
+
|
|
10
|
+
decibri 4.2.0 is a browser-only, additive release. Code written for 4.1.0 keeps
|
|
11
|
+
working unchanged; there is nothing to migrate. The release adds audio playback
|
|
12
|
+
to the browser build:
|
|
13
|
+
|
|
14
|
+
- A browser `Speaker` that plays audio through the Web Audio API: `start()`,
|
|
15
|
+
async `write(chunk)`, async `drain()`, `stop()`, and `isPlaying`, with int16
|
|
16
|
+
and float32 input and resampling from the source rate to the output rate. It
|
|
17
|
+
is started from a user gesture, as browsers require. The browser `Microphone`
|
|
18
|
+
(capture) and the entire Node.js API are unchanged.
|
|
19
|
+
|
|
20
|
+
See the browser Speaker section of the README for examples.
|
|
21
|
+
|
|
8
22
|
## New in 4.1.0 (additive, nothing to migrate)
|
|
9
23
|
|
|
10
24
|
decibri 4.1.0 is a non-breaking, additive release. Code written for 4.0.0 keeps
|
package/README.md
CHANGED
|
@@ -50,6 +50,19 @@ mic.on('data', (chunk) => { /* Int16Array of PCM samples */ });
|
|
|
50
50
|
await mic.start(); // requires user gesture in Safari
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Browser playback
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import { Speaker } from 'decibri'; // browser entry via conditional export
|
|
57
|
+
|
|
58
|
+
const speaker = new Speaker({ sampleRate: 16000 });
|
|
59
|
+
playButton.onclick = async () => {
|
|
60
|
+
await speaker.write(int16Chunk); // Int16Array of PCM samples
|
|
61
|
+
await speaker.drain(); // resolves when playback finishes
|
|
62
|
+
speaker.stop();
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
53
66
|
### Pipe capture to playback (echo)
|
|
54
67
|
|
|
55
68
|
```javascript
|
|
@@ -216,6 +229,37 @@ The browser runs energy-mode VAD only, so its `vad` option accepts `false` or `'
|
|
|
216
229
|
| Sample rate | Native device rate | Resampled from native rate |
|
|
217
230
|
| VAD | `'silero'` or `'energy'` | `'energy'` only |
|
|
218
231
|
|
|
232
|
+
### `new Speaker(options?)` (browser)
|
|
233
|
+
|
|
234
|
+
Browser audio playback through the Web Audio API. Playback is async (Promise based) and must be started from a user gesture so the browser allows audio.
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
import { Speaker } from 'decibri'; // browser entry via conditional export
|
|
238
|
+
|
|
239
|
+
const speaker = new Speaker({ sampleRate: 16000 });
|
|
240
|
+
|
|
241
|
+
playButton.onclick = async () => {
|
|
242
|
+
await speaker.write(int16Chunk); // Int16Array of PCM samples
|
|
243
|
+
await speaker.drain(); // resolves when playback finishes
|
|
244
|
+
speaker.stop();
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
| Option | Type | Default | Description |
|
|
249
|
+
| --- | --- | --- | --- |
|
|
250
|
+
| `sampleRate` | number | 16000 | Sample rate of the audio you write (resampled to the output rate) |
|
|
251
|
+
| `channels` | number | 1 | Output channels (a mono stream plays on every channel) |
|
|
252
|
+
| `dtype` | `'int16'` \| `'float32'` | `'int16'` | Encoding of the samples you write |
|
|
253
|
+
| `workletUrl` | string | inline blob | Custom worklet URL for strict CSP |
|
|
254
|
+
|
|
255
|
+
- `start()` creates and resumes the audio output. Optional: `write()` starts it on the first call. Either must run in a user gesture (a click or tap); a context blocked by the autoplay policy surfaces a clear error.
|
|
256
|
+
- `write(chunk)` resolves when the samples are queued. It waits when the buffer is full, so awaiting it paces playback. Await calls sequentially to preserve order.
|
|
257
|
+
- `drain()` resolves when the queued audio has finished playing, immediately if nothing is queued.
|
|
258
|
+
- `stop()` halts immediately and discards anything queued.
|
|
259
|
+
- `isPlaying` reports whether audio is currently queued and playing.
|
|
260
|
+
|
|
261
|
+
To verify playback in real browsers, open `examples/browser-speaker-test.html` (see `examples/README.md`).
|
|
262
|
+
|
|
219
263
|
## Voice Activity Detection
|
|
220
264
|
|
|
221
265
|
### Energy mode
|
|
@@ -278,6 +322,8 @@ node node_modules/decibri/examples/websocket-server.js # terminal 1
|
|
|
278
322
|
node node_modules/decibri/examples/websocket-stream.js # terminal 2
|
|
279
323
|
```
|
|
280
324
|
|
|
325
|
+
For the browser, `examples/browser-speaker-test.html` is a page for manually verifying audio playback in each browser. See `examples/README.md` for how to serve it on desktop and mobile.
|
|
326
|
+
|
|
281
327
|
## Migrating from 3.x
|
|
282
328
|
|
|
283
329
|
decibri 4.0.0 renames the API to a microphone and speaker vocabulary and switches to named exports. See [MIGRATION.md](./MIGRATION.md) for a complete before-and-after guide.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# decibri examples
|
|
2
|
+
|
|
3
|
+
Runnable examples for decibri. The Node.js examples run with `node`; the browser
|
|
4
|
+
example is an HTML page you open in a browser.
|
|
5
|
+
|
|
6
|
+
## Node.js examples
|
|
7
|
+
|
|
8
|
+
| File | What it does | Notes |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `wav-capture.js` | Captures audio and writes a valid WAV file | No extra dependencies |
|
|
11
|
+
| `websocket-server.js` | Receives raw PCM chunks and logs byte counts | Requires `npm install ws` |
|
|
12
|
+
| `websocket-stream.js` | Streams raw PCM audio to a WebSocket server | Requires `npm install ws` |
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node wav-capture.js
|
|
16
|
+
node websocket-server.js # terminal 1
|
|
17
|
+
node websocket-stream.js # terminal 2
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Browser audio playback test (`browser-speaker-test.html`)
|
|
21
|
+
|
|
22
|
+
A page for manually verifying that the browser Speaker plays real audio in each
|
|
23
|
+
browser. It loads the real browser build (`decibri.browser.js`, a bundle of the
|
|
24
|
+
package's browser entry) and plays generated tones so you can listen for a clean
|
|
25
|
+
signal, glitches, correct pitch after resampling, and immediate stop.
|
|
26
|
+
|
|
27
|
+
Browser audio needs a secure context, so the page must be served over
|
|
28
|
+
`localhost` or `https`. Opening it as a `file://` URL will not load the audio
|
|
29
|
+
engine.
|
|
30
|
+
|
|
31
|
+
### Desktop
|
|
32
|
+
|
|
33
|
+
From this `examples` directory, serve it over localhost and open the printed URL
|
|
34
|
+
in each browser you want to test:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx serve .
|
|
38
|
+
# or
|
|
39
|
+
npx http-server . -p 8080
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then open `http://localhost:<port>/browser-speaker-test.html` in Chrome, Firefox,
|
|
43
|
+
Edge, and Safari, and tap the buttons.
|
|
44
|
+
|
|
45
|
+
### Mobile (same WiFi)
|
|
46
|
+
|
|
47
|
+
Serve on your computer as above, find your machine's LAN IP, and open
|
|
48
|
+
`http://<machine-ip>:<port>/browser-speaker-test.html` on the phone (on the same
|
|
49
|
+
network).
|
|
50
|
+
|
|
51
|
+
iOS Safari treats a plain `http://<LAN-IP>` address as an insecure context and
|
|
52
|
+
will refuse to start audio. For iOS, expose the local server over `https` with a
|
|
53
|
+
tunnel and open the tunnel URL on the phone:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx localtunnel --port 8080
|
|
57
|
+
# open the printed https://... URL on the phone, then add /browser-speaker-test.html
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### What to check in each browser
|
|
61
|
+
|
|
62
|
+
- 440 Hz tone: a clean, steady tone with no buzz or distortion.
|
|
63
|
+
- Continuous (3 s): smooth, with no gaps, clicks, or dropouts.
|
|
64
|
+
- Resampled from 16 kHz: the same pitch as the 440 Hz tone button (confirms
|
|
65
|
+
resampling does not shift pitch).
|
|
66
|
+
- Stop: playback halts immediately when tapped mid-playback.
|
|
67
|
+
- Gesture requirement: nothing plays until you tap a button; if the browser
|
|
68
|
+
blocks audio, a clear error appears in the on-page log.
|
|
69
|
+
- Both formats: repeat with the int16 and float32 toggle.
|
|
70
|
+
|
|
71
|
+
### Regenerating the browser bundle
|
|
72
|
+
|
|
73
|
+
`decibri.browser.js` is generated from the package's browser entry
|
|
74
|
+
(`../src/browser/index.js`). Regenerate it after changing the browser source
|
|
75
|
+
with any bundler that resolves the package's `browser` entry, for example:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx esbuild ../src/browser/index.js --bundle --format=iife --global-name=decibri --outfile=decibri.browser.js
|
|
79
|
+
```
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>decibri browser audio playback test</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: system-ui, sans-serif; max-width: 680px; margin: 40px auto; padding: 0 20px; color: #222; }
|
|
10
|
+
h1 { font-size: 1.4rem; margin-bottom: 8px; }
|
|
11
|
+
p.intro { font-size: 0.9rem; color: #555; margin-bottom: 16px; line-height: 1.5; }
|
|
12
|
+
.format-toggle { margin-bottom: 16px; font-size: 0.9rem; }
|
|
13
|
+
.format-toggle label { margin-right: 12px; cursor: pointer; }
|
|
14
|
+
.controls { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
15
|
+
button { padding: 10px 16px; font-size: 0.9rem; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; background: #f8f8f8; }
|
|
16
|
+
button:hover { background: #e8e8e8; }
|
|
17
|
+
button.stop { border-color: #f44336; color: #c62828; }
|
|
18
|
+
.hint { display: block; font-size: 0.75rem; color: #888; margin-top: 2px; }
|
|
19
|
+
.stats { font-size: 0.85rem; color: #666; margin-bottom: 12px; }
|
|
20
|
+
#log { background: #1a1a1a; color: #0f0; font-family: monospace; font-size: 0.8rem; padding: 12px; border-radius: 4px; height: 320px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<h1>decibri browser audio playback test</h1>
|
|
25
|
+
<p class="intro">
|
|
26
|
+
Tap a button to play audio through the browser Speaker, then listen and watch the log.
|
|
27
|
+
Playback must start from a tap (the browser only allows audio after a user gesture), so
|
|
28
|
+
nothing plays until you tap. Serve this page over <strong>localhost</strong> or
|
|
29
|
+
<strong>https</strong>; opening it as a <code>file://</code> URL will not work. See
|
|
30
|
+
<code>examples/README.md</code> for serve and mobile instructions.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<div class="format-toggle">
|
|
34
|
+
<strong>Format:</strong>
|
|
35
|
+
<label><input type="radio" name="dtype" value="int16" checked> int16</label>
|
|
36
|
+
<label><input type="radio" name="dtype" value="float32"> float32</label>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="controls">
|
|
40
|
+
<div>
|
|
41
|
+
<button id="btn-tone">Play 440 Hz tone</button>
|
|
42
|
+
<span class="hint">about 1.5 s. Should be a clean, steady tone.</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<button id="btn-continuous">Play 3 s continuous</button>
|
|
46
|
+
<span class="hint">Listen for any gaps, clicks, or dropouts.</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<button id="btn-resampled">Play 440 Hz from 16 kHz</button>
|
|
50
|
+
<span class="hint">Forces resampling. Should match the first button's pitch.</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<button id="btn-stop" class="stop">Stop</button>
|
|
54
|
+
<span class="hint">Should halt immediately.</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="stats" id="stats">Idle.</div>
|
|
59
|
+
<div id="log"></div>
|
|
60
|
+
|
|
61
|
+
<!--
|
|
62
|
+
This page loads the real browser Speaker from examples/decibri.browser.js, a
|
|
63
|
+
generated bundle of the package's browser entry (src/browser/index.js). It is
|
|
64
|
+
the actual shipped code, not a copy. See examples/README.md to regenerate it.
|
|
65
|
+
-->
|
|
66
|
+
<script src="./decibri.browser.js"></script>
|
|
67
|
+
<script>
|
|
68
|
+
const { Speaker } = decibri;
|
|
69
|
+
|
|
70
|
+
const logEl = document.getElementById('log');
|
|
71
|
+
const statsEl = document.getElementById('stats');
|
|
72
|
+
|
|
73
|
+
let current = null;
|
|
74
|
+
|
|
75
|
+
function log(msg) {
|
|
76
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
77
|
+
logEl.textContent += `[${ts}] ${msg}\n`;
|
|
78
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setStats(text) {
|
|
82
|
+
const playing = current ? current.isPlaying : false;
|
|
83
|
+
statsEl.textContent = `${text} | isPlaying: ${playing}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getDtype() {
|
|
87
|
+
return document.querySelector('input[name="dtype"]:checked').value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Generate a sine tone at `rate` Hz with short fades to avoid edge clicks.
|
|
91
|
+
function genTone(freq, seconds, rate, dtype) {
|
|
92
|
+
const n = Math.floor(rate * seconds);
|
|
93
|
+
const amp = 0.25;
|
|
94
|
+
const fade = Math.min(Math.floor(rate * 0.005), Math.floor(n / 2));
|
|
95
|
+
const f32 = new Float32Array(n);
|
|
96
|
+
for (let i = 0; i < n; i++) {
|
|
97
|
+
let env = 1;
|
|
98
|
+
if (i < fade) env = i / fade;
|
|
99
|
+
else if (i >= n - fade) env = (n - 1 - i) / fade;
|
|
100
|
+
f32[i] = Math.sin(2 * Math.PI * freq * i / rate) * amp * env;
|
|
101
|
+
}
|
|
102
|
+
if (dtype === 'float32') return f32;
|
|
103
|
+
const i16 = new Int16Array(n);
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
i16[i] = Math.max(-32768, Math.min(32767, Math.round(f32[i] * 32767)));
|
|
106
|
+
}
|
|
107
|
+
return i16;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function stopCurrent() {
|
|
111
|
+
if (current) {
|
|
112
|
+
current.stop();
|
|
113
|
+
log('stop()');
|
|
114
|
+
current = null;
|
|
115
|
+
setStats('Stopped.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function play(label, freq, seconds, srcRate) {
|
|
120
|
+
stopCurrent();
|
|
121
|
+
const dtype = getDtype();
|
|
122
|
+
log(`${label}: ${seconds}s of ${freq} Hz, source ${srcRate} Hz, dtype=${dtype}`);
|
|
123
|
+
setStats('Starting...');
|
|
124
|
+
const speaker = new Speaker({ sampleRate: srcRate, dtype });
|
|
125
|
+
current = speaker;
|
|
126
|
+
try {
|
|
127
|
+
const tone = genTone(freq, seconds, srcRate, dtype);
|
|
128
|
+
await speaker.write(tone);
|
|
129
|
+
log(`queued ${tone.length} samples, waiting for playback to finish...`);
|
|
130
|
+
setStats('Playing...');
|
|
131
|
+
await speaker.drain();
|
|
132
|
+
log('drained (playback finished)');
|
|
133
|
+
setStats('Finished.');
|
|
134
|
+
// Only tear down if a newer play has not replaced this one.
|
|
135
|
+
if (current === speaker) {
|
|
136
|
+
speaker.stop();
|
|
137
|
+
current = null;
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log('ERROR: ' + (err && err.message ? err.message : String(err)));
|
|
141
|
+
setStats('Error (see log).');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
document.getElementById('btn-tone').addEventListener('click', () => {
|
|
146
|
+
play('440 Hz tone', 440, 1.5, 44100);
|
|
147
|
+
});
|
|
148
|
+
document.getElementById('btn-continuous').addEventListener('click', () => {
|
|
149
|
+
play('continuous', 440, 3, 44100);
|
|
150
|
+
});
|
|
151
|
+
document.getElementById('btn-resampled').addEventListener('click', () => {
|
|
152
|
+
play('resampled from 16 kHz', 440, 1.5, 16000);
|
|
153
|
+
});
|
|
154
|
+
document.getElementById('btn-stop').addEventListener('click', stopCurrent);
|
|
155
|
+
|
|
156
|
+
log('Ready. Tap a button to play.');
|
|
157
|
+
</script>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|