decibri 1.0.0 → 3.0.0-rc.2
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/models/silero_vad.onnx +0 -0
- package/package.json +68 -44
- package/src/browser/decibri-browser.js +312 -0
- package/src/browser/emitter.js +66 -0
- package/src/browser/index.d.ts +155 -0
- package/src/browser/index.js +6 -0
- package/src/browser/worklet-inline.js +14 -0
- package/src/browser/worklet-processor.js +110 -0
- package/src/decibri-output.js +129 -0
- package/src/decibri.d.ts +237 -0
- package/src/decibri.js +221 -0
- package/LICENSE +0 -191
- package/README.md +0 -294
- package/binding.gyp +0 -122
- package/examples/wav-capture.js +0 -48
- package/examples/websocket-server.js +0 -28
- package/examples/websocket-stream.js +0 -44
- package/index.js +0 -190
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/linux-arm64/node.napi.node +0 -0
- package/prebuilds/linux-x64/node.napi.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/src/decibri.cc +0 -390
- package/src/mac_permission.mm +0 -21
- package/types/index.d.ts +0 -273
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,60 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decibri",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Cross-platform
|
|
5
|
-
"main": "
|
|
6
|
-
"types": "
|
|
3
|
+
"version": "3.0.0-rc.2",
|
|
4
|
+
"description": "Cross-platform audio capture, output, and processing for Node.js and browsers",
|
|
5
|
+
"main": "src/decibri.js",
|
|
6
|
+
"types": "src/decibri.d.ts",
|
|
7
|
+
"browser": "./src/browser/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"node": {
|
|
11
|
+
"types": "./src/decibri.d.ts",
|
|
12
|
+
"require": "./src/decibri.js",
|
|
13
|
+
"import": "./src/decibri.js"
|
|
14
|
+
},
|
|
15
|
+
"browser": {
|
|
16
|
+
"types": "./src/browser/index.d.ts",
|
|
17
|
+
"default": "./src/browser/index.js"
|
|
18
|
+
},
|
|
19
|
+
"default": "./src/decibri.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
7
22
|
"license": "Apache-2.0",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"url": "git+https://github.com/analyticsinmotion/decibri.git"
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
11
25
|
},
|
|
12
|
-
"
|
|
26
|
+
"os": [
|
|
27
|
+
"win32",
|
|
28
|
+
"darwin",
|
|
29
|
+
"linux"
|
|
30
|
+
],
|
|
31
|
+
"cpu": [
|
|
32
|
+
"x64",
|
|
33
|
+
"arm64"
|
|
34
|
+
],
|
|
13
35
|
"keywords": [
|
|
14
|
-
"microphone",
|
|
15
36
|
"audio",
|
|
37
|
+
"microphone",
|
|
16
38
|
"capture",
|
|
17
|
-
"
|
|
39
|
+
"sound",
|
|
40
|
+
"voice",
|
|
41
|
+
"speech",
|
|
42
|
+
"recording",
|
|
18
43
|
"pcm",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"prebuilt",
|
|
22
|
-
"voice-ai",
|
|
23
|
-
"speech-to-text",
|
|
24
|
-
"wake-word",
|
|
25
|
-
"real-time-audio"
|
|
44
|
+
"stream",
|
|
45
|
+
"real-time"
|
|
26
46
|
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/decibri/decibri.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://decibri.com",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/decibri/decibri/issues"
|
|
54
|
+
},
|
|
55
|
+
"napi": {
|
|
56
|
+
"binaryName": "decibri",
|
|
57
|
+
"targets": [
|
|
58
|
+
"x86_64-pc-windows-msvc",
|
|
59
|
+
"aarch64-apple-darwin",
|
|
60
|
+
"x86_64-unknown-linux-gnu",
|
|
61
|
+
"aarch64-unknown-linux-gnu"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
27
64
|
"files": [
|
|
28
|
-
"index.js",
|
|
29
|
-
"types/",
|
|
30
|
-
"examples/",
|
|
31
|
-
"binding.gyp",
|
|
32
65
|
"src/",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
66
|
+
"models/",
|
|
67
|
+
"index.js",
|
|
68
|
+
"index.d.ts"
|
|
35
69
|
],
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"node-gyp-build": "^4.6.0"
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"@decibri/win32-x64-msvc": "3.0.0-rc.2",
|
|
72
|
+
"@decibri/darwin-arm64": "3.0.0-rc.2",
|
|
73
|
+
"@decibri/linux-x64-gnu": "3.0.0-rc.2",
|
|
74
|
+
"@decibri/linux-arm64-gnu": "3.0.0-rc.2"
|
|
43
75
|
},
|
|
44
76
|
"devDependencies": {
|
|
45
|
-
"
|
|
46
|
-
"node-gyp": "^12.0.0"
|
|
47
|
-
},
|
|
48
|
-
"engines": {
|
|
49
|
-
"node": ">=18.0.0"
|
|
77
|
+
"@napi-rs/cli": "^3.0.0"
|
|
50
78
|
},
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"cpu": [
|
|
57
|
-
"x64",
|
|
58
|
-
"arm64"
|
|
59
|
-
]
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "napi build --platform --release -p decibri-node --manifest-path ../../bindings/node/Cargo.toml -o .",
|
|
81
|
+
"artifacts": "napi artifacts",
|
|
82
|
+
"version": "napi version"
|
|
83
|
+
}
|
|
60
84
|
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Emitter } = require('./emitter.js');
|
|
4
|
+
const { WORKLET_SOURCE } = require('./worklet-inline.js');
|
|
5
|
+
|
|
6
|
+
const VERSION = '3.0.0-rc.2';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Browser microphone capture.
|
|
10
|
+
*
|
|
11
|
+
* Uses getUserMedia + AudioWorklet for real-time audio capture in browsers.
|
|
12
|
+
* Emits 'data' events with Int16Array or Float32Array chunks.
|
|
13
|
+
*
|
|
14
|
+
* Ported from decibri-web decibri.ts. Logic identical, types removed.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { Decibri } = require('decibri'); // browser entry via conditional export
|
|
18
|
+
* const mic = new Decibri({ sampleRate: 16000 });
|
|
19
|
+
* mic.on('data', (chunk) => { // chunk is Int16Array });
|
|
20
|
+
* await mic.start();
|
|
21
|
+
* // later...
|
|
22
|
+
* mic.stop();
|
|
23
|
+
*/
|
|
24
|
+
class Decibri extends Emitter {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
// ── Private state ─────────────────────────────────────────────────────
|
|
29
|
+
this._audioContext = null;
|
|
30
|
+
this._stream = null;
|
|
31
|
+
this._sourceNode = null;
|
|
32
|
+
this._workletNode = null;
|
|
33
|
+
this._started = false;
|
|
34
|
+
this._starting = null;
|
|
35
|
+
this._stopRequested = false;
|
|
36
|
+
|
|
37
|
+
// ── VAD state ─────────────────────────────────────────────────────────
|
|
38
|
+
this._vad = options.vad ?? false;
|
|
39
|
+
this._vadThreshold = options.vadThreshold ?? 0.01;
|
|
40
|
+
this._vadHoldoff = options.vadHoldoff ?? 300;
|
|
41
|
+
this._isSpeaking = false;
|
|
42
|
+
this._silenceTimer = null;
|
|
43
|
+
|
|
44
|
+
// ── Options ───────────────────────────────────────────────────────────
|
|
45
|
+
this._sampleRate = options.sampleRate ?? 16000;
|
|
46
|
+
this._channels = options.channels ?? 1;
|
|
47
|
+
this._framesPerBuffer = options.framesPerBuffer ?? 1600;
|
|
48
|
+
this._device = options.device;
|
|
49
|
+
this._format = options.format ?? 'int16';
|
|
50
|
+
this._echoCancellation = options.echoCancellation ?? true;
|
|
51
|
+
this._noiseSuppression = options.noiseSuppression ?? true;
|
|
52
|
+
this._workletUrl = options.workletUrl;
|
|
53
|
+
|
|
54
|
+
// ── Validate ──────────────────────────────────────────────────────────
|
|
55
|
+
if (this._sampleRate < 1000 || this._sampleRate > 384000) {
|
|
56
|
+
throw new TypeError(`sampleRate must be between 1000 and 384000, got ${this._sampleRate}`);
|
|
57
|
+
}
|
|
58
|
+
if (this._channels < 1 || this._channels > 32) {
|
|
59
|
+
throw new TypeError(`channels must be between 1 and 32, got ${this._channels}`);
|
|
60
|
+
}
|
|
61
|
+
if (this._framesPerBuffer < 64 || this._framesPerBuffer > 65536) {
|
|
62
|
+
throw new TypeError(`framesPerBuffer must be between 64 and 65536, got ${this._framesPerBuffer}`);
|
|
63
|
+
}
|
|
64
|
+
if (this._format !== 'int16' && this._format !== 'float32') {
|
|
65
|
+
throw new TypeError("format must be 'int16' or 'float32'");
|
|
66
|
+
}
|
|
67
|
+
if (this._vadThreshold < 0 || this._vadThreshold > 1) {
|
|
68
|
+
throw new TypeError(`vadThreshold must be between 0 and 1, got ${this._vadThreshold}`);
|
|
69
|
+
}
|
|
70
|
+
if (this._vadHoldoff < 0) {
|
|
71
|
+
throw new TypeError(`vadHoldoff must be >= 0, got ${this._vadHoldoff}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start microphone capture.
|
|
79
|
+
* Requests microphone permission and sets up the audio pipeline.
|
|
80
|
+
* Must be called from a user gesture context in Safari.
|
|
81
|
+
* No-op if already started. Returns the existing promise if a start
|
|
82
|
+
* is already in progress.
|
|
83
|
+
*/
|
|
84
|
+
start() {
|
|
85
|
+
if (this._started) return Promise.resolve();
|
|
86
|
+
if (this._starting) return this._starting;
|
|
87
|
+
|
|
88
|
+
this._starting = this._doStart().finally(() => {
|
|
89
|
+
this._starting = null;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return this._starting;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stop microphone capture and release all resources.
|
|
97
|
+
* Safe to call multiple times or before start().
|
|
98
|
+
* After stop(), calling start() again creates a fresh session.
|
|
99
|
+
*/
|
|
100
|
+
stop() {
|
|
101
|
+
if (!this._started) {
|
|
102
|
+
if (this._starting) {
|
|
103
|
+
this._stopRequested = true;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this._started = false;
|
|
108
|
+
|
|
109
|
+
// Stop media tracks
|
|
110
|
+
if (this._stream) {
|
|
111
|
+
this._stream.getTracks().forEach(t => t.stop());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Disconnect audio nodes
|
|
115
|
+
if (this._sourceNode) this._sourceNode.disconnect();
|
|
116
|
+
if (this._workletNode) {
|
|
117
|
+
this._workletNode.disconnect();
|
|
118
|
+
this._workletNode.port.close();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Close audio context
|
|
122
|
+
if (this._audioContext) this._audioContext.close();
|
|
123
|
+
|
|
124
|
+
// Clear VAD
|
|
125
|
+
if (this._silenceTimer !== null) {
|
|
126
|
+
clearTimeout(this._silenceTimer);
|
|
127
|
+
this._silenceTimer = null;
|
|
128
|
+
}
|
|
129
|
+
this._isSpeaking = false;
|
|
130
|
+
|
|
131
|
+
// Release references
|
|
132
|
+
this._audioContext = null;
|
|
133
|
+
this._stream = null;
|
|
134
|
+
this._sourceNode = null;
|
|
135
|
+
this._workletNode = null;
|
|
136
|
+
|
|
137
|
+
this.emit('end');
|
|
138
|
+
this.emit('close');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Whether the microphone is currently capturing. */
|
|
142
|
+
get isOpen() {
|
|
143
|
+
return this._started;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* List available audio input devices.
|
|
148
|
+
* Device labels may be empty until microphone permission is granted.
|
|
149
|
+
*/
|
|
150
|
+
static async devices() {
|
|
151
|
+
const all = await navigator.mediaDevices.enumerateDevices();
|
|
152
|
+
return all
|
|
153
|
+
.filter(d => d.kind === 'audioinput')
|
|
154
|
+
.map(d => ({
|
|
155
|
+
deviceId: d.deviceId,
|
|
156
|
+
label: d.label,
|
|
157
|
+
groupId: d.groupId,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Version information. */
|
|
162
|
+
static version() {
|
|
163
|
+
return { decibri: VERSION };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
async _doStart() {
|
|
169
|
+
// 1. Create AudioContext at native sample rate
|
|
170
|
+
this._audioContext = new AudioContext();
|
|
171
|
+
const nativeSampleRate = this._audioContext.sampleRate;
|
|
172
|
+
|
|
173
|
+
// Safari fix: resume suspended AudioContext
|
|
174
|
+
await this._audioContext.resume();
|
|
175
|
+
|
|
176
|
+
// 2. Request microphone access
|
|
177
|
+
const audioConstraints = {
|
|
178
|
+
channelCount: this._channels,
|
|
179
|
+
echoCancellation: this._echoCancellation,
|
|
180
|
+
noiseSuppression: this._noiseSuppression,
|
|
181
|
+
};
|
|
182
|
+
if (this._device) {
|
|
183
|
+
audioConstraints.deviceId = { exact: this._device };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
this._stream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints });
|
|
188
|
+
} catch (err) {
|
|
189
|
+
await this._audioContext.close();
|
|
190
|
+
this._audioContext = null;
|
|
191
|
+
const error = this._mapError(err);
|
|
192
|
+
this.emit('error', error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 3. Load AudioWorklet processor
|
|
197
|
+
let blobUrl = null;
|
|
198
|
+
const workletUrl = this._workletUrl ?? (blobUrl = this._createBlobUrl());
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await this._audioContext.audioWorklet.addModule(workletUrl);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
204
|
+
this._stream.getTracks().forEach(t => t.stop());
|
|
205
|
+
this._stream = null;
|
|
206
|
+
await this._audioContext.close();
|
|
207
|
+
this._audioContext = null;
|
|
208
|
+
const error = new Error('Failed to load audio worklet: ' + (err instanceof Error ? err.message : String(err)));
|
|
209
|
+
this.emit('error', error);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
214
|
+
|
|
215
|
+
// 4. Build audio graph
|
|
216
|
+
this._sourceNode = this._audioContext.createMediaStreamSource(this._stream);
|
|
217
|
+
this._workletNode = new AudioWorkletNode(this._audioContext, 'decibri-processor', {
|
|
218
|
+
processorOptions: {
|
|
219
|
+
framesPerBuffer: this._framesPerBuffer,
|
|
220
|
+
format: this._format,
|
|
221
|
+
nativeSampleRate,
|
|
222
|
+
targetSampleRate: this._sampleRate,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// 5. Wire up data from worklet
|
|
227
|
+
this._workletNode.port.onmessage = (event) => {
|
|
228
|
+
const buffer = event.data;
|
|
229
|
+
const chunk = this._format === 'int16'
|
|
230
|
+
? new Int16Array(buffer)
|
|
231
|
+
: new Float32Array(buffer);
|
|
232
|
+
|
|
233
|
+
this.emit('data', chunk);
|
|
234
|
+
|
|
235
|
+
if (this._vad) {
|
|
236
|
+
this._processVad(chunk);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// 6. Connect (source → worklet, NOT to destination)
|
|
241
|
+
this._sourceNode.connect(this._workletNode);
|
|
242
|
+
|
|
243
|
+
this._started = true;
|
|
244
|
+
|
|
245
|
+
// If stop() was called while start() was in-flight, tear down now
|
|
246
|
+
if (this._stopRequested) {
|
|
247
|
+
this._stopRequested = false;
|
|
248
|
+
this.stop();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_createBlobUrl() {
|
|
253
|
+
const blob = new Blob([WORKLET_SOURCE], { type: 'application/javascript' });
|
|
254
|
+
return URL.createObjectURL(blob);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_mapError(err) {
|
|
258
|
+
if (err instanceof DOMException) {
|
|
259
|
+
switch (err.name) {
|
|
260
|
+
case 'NotAllowedError':
|
|
261
|
+
return new Error('Microphone permission denied');
|
|
262
|
+
case 'NotFoundError':
|
|
263
|
+
return new Error('No microphone found');
|
|
264
|
+
default:
|
|
265
|
+
return new Error('Microphone access failed: ' + err.message);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── VAD ─────────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
_processVad(chunk) {
|
|
274
|
+
const rms = this._computeRms(chunk);
|
|
275
|
+
|
|
276
|
+
if (rms >= this._vadThreshold) {
|
|
277
|
+
if (this._silenceTimer !== null) {
|
|
278
|
+
clearTimeout(this._silenceTimer);
|
|
279
|
+
this._silenceTimer = null;
|
|
280
|
+
}
|
|
281
|
+
if (!this._isSpeaking) {
|
|
282
|
+
this._isSpeaking = true;
|
|
283
|
+
this.emit('speech');
|
|
284
|
+
}
|
|
285
|
+
} else if (this._isSpeaking && this._silenceTimer === null) {
|
|
286
|
+
this._silenceTimer = setTimeout(() => {
|
|
287
|
+
this._isSpeaking = false;
|
|
288
|
+
this._silenceTimer = null;
|
|
289
|
+
this.emit('silence');
|
|
290
|
+
}, this._vadHoldoff);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_computeRms(chunk) {
|
|
295
|
+
let sum = 0;
|
|
296
|
+
const n = chunk.length;
|
|
297
|
+
if (n === 0) return 0;
|
|
298
|
+
|
|
299
|
+
if (chunk instanceof Float32Array) {
|
|
300
|
+
for (let i = 0; i < n; i++) sum += chunk[i] * chunk[i];
|
|
301
|
+
} else {
|
|
302
|
+
for (let i = 0; i < n; i++) {
|
|
303
|
+
const s = chunk[i] / 32768;
|
|
304
|
+
sum += s * s;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return Math.sqrt(sum / n);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = { Decibri };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal event emitter for browsers.
|
|
5
|
+
*
|
|
6
|
+
* Provides .on() / .off() / .once() / .emit() with the same API pattern
|
|
7
|
+
* as Node.js EventEmitter. Used instead of browser-native EventTarget to
|
|
8
|
+
* preserve API parity with the Node.js decibri (.on('data', cb) pattern).
|
|
9
|
+
*
|
|
10
|
+
* Ported from decibri-web emitter.ts. Logic identical, types removed.
|
|
11
|
+
*/
|
|
12
|
+
class Emitter {
|
|
13
|
+
constructor() {
|
|
14
|
+
this._listeners = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
on(event, fn) {
|
|
18
|
+
let set = this._listeners.get(event);
|
|
19
|
+
if (!set) {
|
|
20
|
+
set = new Set();
|
|
21
|
+
this._listeners.set(event, set);
|
|
22
|
+
}
|
|
23
|
+
set.add(fn);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
off(event, fn) {
|
|
28
|
+
const set = this._listeners.get(event);
|
|
29
|
+
if (!set) return this;
|
|
30
|
+
if (set.delete(fn)) return this;
|
|
31
|
+
for (const listener of set) {
|
|
32
|
+
if (listener._original === fn) {
|
|
33
|
+
set.delete(listener);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
once(event, fn) {
|
|
41
|
+
const wrapper = (...args) => {
|
|
42
|
+
this.off(event, wrapper);
|
|
43
|
+
fn(...args);
|
|
44
|
+
};
|
|
45
|
+
wrapper._original = fn;
|
|
46
|
+
return this.on(event, wrapper);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
emit(event, ...args) {
|
|
50
|
+
const set = this._listeners.get(event);
|
|
51
|
+
if (!set || set.size === 0) return false;
|
|
52
|
+
for (const fn of set) fn(...args);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
removeAllListeners(event) {
|
|
57
|
+
if (event !== undefined) {
|
|
58
|
+
this._listeners.delete(event);
|
|
59
|
+
} else {
|
|
60
|
+
this._listeners.clear();
|
|
61
|
+
}
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { Emitter };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/** Information about an available audio input device (browser). */
|
|
2
|
+
export interface DeviceInfo {
|
|
3
|
+
/** Opaque device identifier (pass to constructor as `device`). */
|
|
4
|
+
deviceId: string;
|
|
5
|
+
/** Human-readable device name. May be empty until permission is granted. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Group identifier. Devices sharing the same physical device. */
|
|
8
|
+
groupId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Version information. */
|
|
12
|
+
export interface VersionInfo {
|
|
13
|
+
/** decibri package version (e.g. `"3.0.0"`). */
|
|
14
|
+
decibri: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Constructor options for the browser `Decibri` class. */
|
|
18
|
+
export interface DecibriOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Target sample rate in Hz. Browser captures at native rate and resamples.
|
|
21
|
+
* @default 16000
|
|
22
|
+
* @range 1000–384000
|
|
23
|
+
*/
|
|
24
|
+
sampleRate?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Number of input channels.
|
|
28
|
+
* @default 1
|
|
29
|
+
* @range 1–32
|
|
30
|
+
*/
|
|
31
|
+
channels?: number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Frames per audio chunk. Controls chunk size and delivery interval.
|
|
35
|
+
* @default 1600
|
|
36
|
+
* @range 64–65536
|
|
37
|
+
*/
|
|
38
|
+
framesPerBuffer?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Audio input device. Pass a deviceId string from `Decibri.devices()`.
|
|
42
|
+
* Omit to use the system default input device.
|
|
43
|
+
*/
|
|
44
|
+
device?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sample encoding format.
|
|
48
|
+
* - `'int16'`: Int16Array of PCM samples
|
|
49
|
+
* - `'float32'`: Float32Array of samples in [-1, 1]
|
|
50
|
+
* @default 'int16'
|
|
51
|
+
*/
|
|
52
|
+
format?: 'int16' | 'float32';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enable energy-based voice activity detection.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
vad?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* RMS energy threshold for speech detection (VAD mode only).
|
|
62
|
+
* @default 0.01
|
|
63
|
+
* @range 0–1
|
|
64
|
+
*/
|
|
65
|
+
vadThreshold?: number;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Milliseconds of sub-threshold audio before emitting `'silence'`.
|
|
69
|
+
* @default 300
|
|
70
|
+
*/
|
|
71
|
+
vadHoldoff?: number;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Enable browser echo cancellation.
|
|
75
|
+
* @default true
|
|
76
|
+
*/
|
|
77
|
+
echoCancellation?: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enable browser noise suppression.
|
|
81
|
+
* @default true
|
|
82
|
+
*/
|
|
83
|
+
noiseSuppression?: boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Custom URL for the AudioWorklet processor script.
|
|
87
|
+
* Use this for strict CSP environments that block blob URLs.
|
|
88
|
+
* Omit to use the default inline blob URL.
|
|
89
|
+
*/
|
|
90
|
+
workletUrl?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Browser microphone capture via getUserMedia + AudioWorklet.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```js
|
|
98
|
+
* import { Decibri } from 'decibri'; // browser entry via conditional export
|
|
99
|
+
*
|
|
100
|
+
* const mic = new Decibri({ sampleRate: 16000 });
|
|
101
|
+
* mic.on('data', (chunk) => {
|
|
102
|
+
* // chunk is an Int16Array of PCM samples
|
|
103
|
+
* });
|
|
104
|
+
* await mic.start(); // call from a user gesture in Safari
|
|
105
|
+
* mic.stop();
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare class Decibri {
|
|
109
|
+
constructor(options?: DecibriOptions);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Start microphone capture.
|
|
113
|
+
* Requests permission and sets up the audio pipeline.
|
|
114
|
+
* Must be called from a user gesture context in Safari.
|
|
115
|
+
*/
|
|
116
|
+
start(): Promise<void>;
|
|
117
|
+
|
|
118
|
+
/** Stop capture and release all resources. Safe to call multiple times. */
|
|
119
|
+
stop(): void;
|
|
120
|
+
|
|
121
|
+
/** Whether the microphone is currently capturing. */
|
|
122
|
+
readonly isOpen: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* List available audio input devices.
|
|
126
|
+
* Labels may be empty until microphone permission is granted.
|
|
127
|
+
*/
|
|
128
|
+
static devices(): Promise<DeviceInfo[]>;
|
|
129
|
+
|
|
130
|
+
/** Version information. */
|
|
131
|
+
static version(): VersionInfo;
|
|
132
|
+
|
|
133
|
+
// ── Event methods ──────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
on(event: 'data', listener: (chunk: Int16Array | Float32Array) => void): this;
|
|
136
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
137
|
+
on(event: 'end', listener: () => void): this;
|
|
138
|
+
on(event: 'close', listener: () => void): this;
|
|
139
|
+
on(event: 'speech', listener: () => void): this;
|
|
140
|
+
on(event: 'silence', listener: () => void): this;
|
|
141
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
142
|
+
|
|
143
|
+
off(event: string, listener: (...args: any[]) => void): this;
|
|
144
|
+
|
|
145
|
+
once(event: 'data', listener: (chunk: Int16Array | Float32Array) => void): this;
|
|
146
|
+
once(event: 'error', listener: (err: Error) => void): this;
|
|
147
|
+
once(event: 'end', listener: () => void): this;
|
|
148
|
+
once(event: 'close', listener: () => void): this;
|
|
149
|
+
once(event: 'speech', listener: () => void): this;
|
|
150
|
+
once(event: 'silence', listener: () => void): this;
|
|
151
|
+
once(event: string, listener: (...args: any[]) => void): this;
|
|
152
|
+
|
|
153
|
+
emit(event: string, ...args: any[]): boolean;
|
|
154
|
+
removeAllListeners(event?: string): this;
|
|
155
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minified AudioWorklet processor source, embedded as a string for Blob URL loading.
|
|
5
|
+
*
|
|
6
|
+
* THIS IS THE CODE THAT ACTUALLY RUNS IN THE BROWSER.
|
|
7
|
+
* The readable version is in worklet-processor.js (documentation/reference only).
|
|
8
|
+
* If worklet-processor.js logic changes, this string MUST be regenerated.
|
|
9
|
+
*
|
|
10
|
+
* Generated from decibri-web v0.1.1 build output. Logic identical to worklet-processor.js.
|
|
11
|
+
*/
|
|
12
|
+
const WORKLET_SOURCE = "var u=class extends AudioWorkletProcessor{constructor(r){super();let e=r.processorOptions;this.framesPerBuffer=e.framesPerBuffer,this.format=e.format,this.ratio=e.nativeSampleRate/e.targetSampleRate,this.needsResample=e.nativeSampleRate!==e.targetSampleRate,this.position=0,this.buffer=new Float32Array(this.framesPerBuffer),this.bufferIndex=0}process(r,e,s){let t=r[0]?.[0];if(!t||t.length===0)return!0;let f;this.needsResample?f=this.resample(t):f=t;let a=0;for(;a<f.length;){let i=this.framesPerBuffer-this.bufferIndex,o=f.length-a,n=Math.min(i,o);this.buffer.set(f.subarray(a,a+n),this.bufferIndex),this.bufferIndex+=n,a+=n,this.bufferIndex>=this.framesPerBuffer&&this.flush()}return!0}resample(r){let e=r.length,s=0,t=this.position;for(;t<e-1;)s++,t+=this.ratio;let f=new Float32Array(s);t=this.position;for(let a=0;a<s;a++){let i=Math.floor(t),o=t-i;f[a]=r[i]*(1-o)+r[i+1]*o,t+=this.ratio}return this.position=Math.max(0,t-e),f}flush(){let r;if(this.format===\"int16\"){let e=new Int16Array(this.framesPerBuffer);for(let s=0;s<this.framesPerBuffer;s++)e[s]=Math.max(-32768,Math.min(32767,Math.round(this.buffer[s]*32768)));r=e.buffer}else r=this.buffer.slice(0,this.framesPerBuffer).buffer;this.port.postMessage(r,[r]),this.buffer=new Float32Array(this.framesPerBuffer),this.bufferIndex=0}};registerProcessor(\"decibri-processor\",u);\n";
|
|
13
|
+
|
|
14
|
+
module.exports = { WORKLET_SOURCE };
|