decibri 0.0.1 → 3.0.0-rc.1

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.
Binary file
package/package.json CHANGED
@@ -1,11 +1,85 @@
1
1
  {
2
2
  "name": "decibri",
3
- "version": "0.0.1",
4
- "description": "Cross-platform audio capture for Node.js. Placeholder - full release coming soon.",
5
- "keywords": ["audio", "microphone", "capture", "portaudio", "stream", "pcm"],
3
+ "version": "3.0.0-rc.1",
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
+ },
6
22
  "license": "Apache-2.0",
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "os": [
27
+ "win32",
28
+ "darwin",
29
+ "linux"
30
+ ],
31
+ "cpu": [
32
+ "x64",
33
+ "arm64"
34
+ ],
35
+ "keywords": [
36
+ "audio",
37
+ "microphone",
38
+ "capture",
39
+ "sound",
40
+ "voice",
41
+ "speech",
42
+ "recording",
43
+ "pcm",
44
+ "stream",
45
+ "real-time"
46
+ ],
7
47
  "repository": {
8
48
  "type": "git",
9
- "url": "https://github.com/analyticsinmotion/micstream.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
+ },
64
+ "files": [
65
+ "src/",
66
+ "models/",
67
+ "index.js",
68
+ "index.d.ts"
69
+ ],
70
+ "optionalDependencies": {
71
+ "decibri-win32-x64-msvc": "3.0.0-rc.1",
72
+ "decibri-darwin-arm64": "3.0.0-rc.1",
73
+ "decibri-linux-x64-gnu": "3.0.0-rc.1",
74
+ "decibri-linux-arm64-gnu": "3.0.0-rc.1"
75
+ },
76
+ "devDependencies": {
77
+ "@napi-rs/cli": "^3.0.0"
78
+ },
79
+ "scripts": {
80
+ "build": "napi build --platform --release -p decibri-node --manifest-path ../../bindings/node/Cargo.toml -o .",
81
+ "artifacts": "napi artifacts",
82
+ "prepublishOnly": "napi prepublish -t npm",
83
+ "version": "napi version"
10
84
  }
11
85
  }
@@ -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.1';
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,6 @@
1
+ 'use strict';
2
+
3
+ const { Decibri } = require('./decibri-browser.js');
4
+ const { Emitter } = require('./emitter.js');
5
+
6
+ module.exports = { Decibri, Emitter };
@@ -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 };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * AudioWorklet processor for decibri browser capture.
3
+ *
4
+ * THIS FILE IS THE READABLE SOURCE. It is NOT loaded at runtime.
5
+ * The minified version in worklet-inline.js is what actually runs.
6
+ * If you change logic here, you MUST regenerate worklet-inline.js.
7
+ *
8
+ * Runs in a dedicated audio thread. Receives Float32 samples at the
9
+ * browser's native sample rate, resamples to the target rate via linear
10
+ * interpolation, optionally converts to Int16, and posts chunks to the
11
+ * main thread.
12
+ *
13
+ * This file cannot import other modules (AudioWorklet restriction).
14
+ *
15
+ * Ported from decibri-web worklet-processor.ts. Logic identical, types removed.
16
+ */
17
+
18
+ class DecibriProcessor extends AudioWorkletProcessor {
19
+ constructor(options) {
20
+ super();
21
+ const opts = options.processorOptions;
22
+ this.framesPerBuffer = opts.framesPerBuffer;
23
+ this.format = opts.format;
24
+ this.ratio = opts.nativeSampleRate / opts.targetSampleRate;
25
+ this.needsResample = opts.nativeSampleRate !== opts.targetSampleRate;
26
+ this.position = 0;
27
+ this.buffer = new Float32Array(this.framesPerBuffer);
28
+ this.bufferIndex = 0;
29
+ }
30
+
31
+ process(inputs, _outputs, _parameters) {
32
+ const input = inputs[0]?.[0];
33
+ if (!input || input.length === 0) return true;
34
+
35
+ let samples;
36
+
37
+ if (this.needsResample) {
38
+ samples = this.resample(input);
39
+ } else {
40
+ samples = input;
41
+ }
42
+
43
+ // Accumulate resampled frames into the buffer
44
+ let offset = 0;
45
+ while (offset < samples.length) {
46
+ const remaining = this.framesPerBuffer - this.bufferIndex;
47
+ const available = samples.length - offset;
48
+ const toCopy = Math.min(remaining, available);
49
+
50
+ this.buffer.set(samples.subarray(offset, offset + toCopy), this.bufferIndex);
51
+ this.bufferIndex += toCopy;
52
+ offset += toCopy;
53
+
54
+ if (this.bufferIndex >= this.framesPerBuffer) {
55
+ this.flush();
56
+ }
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ resample(input) {
63
+ const inputLength = input.length;
64
+
65
+ // Calculate how many output samples we can produce
66
+ let count = 0;
67
+ let pos = this.position;
68
+ while (pos < inputLength - 1) {
69
+ count++;
70
+ pos += this.ratio;
71
+ }
72
+
73
+ const output = new Float32Array(count);
74
+ pos = this.position;
75
+
76
+ for (let i = 0; i < count; i++) {
77
+ const idx = Math.floor(pos);
78
+ const frac = pos - idx;
79
+ output[i] = input[idx] * (1 - frac) + input[idx + 1] * frac;
80
+ pos += this.ratio;
81
+ }
82
+
83
+ // Carry fractional remainder relative to consumed input.
84
+ this.position = Math.max(0, pos - inputLength);
85
+
86
+ return output;
87
+ }
88
+
89
+ flush() {
90
+ let transferBuffer;
91
+
92
+ if (this.format === 'int16') {
93
+ const int16 = new Int16Array(this.framesPerBuffer);
94
+ for (let i = 0; i < this.framesPerBuffer; i++) {
95
+ int16[i] = Math.max(-32768, Math.min(32767, Math.round(this.buffer[i] * 32768)));
96
+ }
97
+ transferBuffer = int16.buffer;
98
+ } else {
99
+ transferBuffer = this.buffer.slice(0, this.framesPerBuffer).buffer;
100
+ }
101
+
102
+ this.port.postMessage(transferBuffer, [transferBuffer]);
103
+
104
+ // Reset accumulation buffer
105
+ this.buffer = new Float32Array(this.framesPerBuffer);
106
+ this.bufferIndex = 0;
107
+ }
108
+ }
109
+
110
+ registerProcessor('decibri-processor', DecibriProcessor);
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const { Writable } = require('stream');
4
+ const { DecibriOutputBridge } = require('../index.js');
5
+
6
+ // ─── DecibriOutput (Writable) ───────────────────────────────────────────────
7
+
8
+ class DecibriOutput extends Writable {
9
+ /**
10
+ * @param {import('./decibri').DecibriOutputOptions} [options]
11
+ */
12
+ constructor(options = {}) {
13
+ super({ highWaterMark: options.highWaterMark || 16384 });
14
+
15
+ // ── Validate options ───────────────────────────────────────────────────
16
+
17
+ const sampleRate = options.sampleRate ?? 16000;
18
+ if (sampleRate < 1000 || sampleRate > 384000) {
19
+ throw new RangeError('sampleRate must be between 1000 and 384000');
20
+ }
21
+
22
+ const channels = options.channels ?? 1;
23
+ if (channels < 1 || channels > 32) {
24
+ throw new RangeError('channels must be between 1 and 32');
25
+ }
26
+
27
+ const format = options.format ?? 'int16';
28
+ if (format !== 'int16' && format !== 'float32') {
29
+ throw new TypeError("format must be 'int16' or 'float32'");
30
+ }
31
+
32
+ // ── Resolve device ──────────────────────────────────────────────────────
33
+
34
+ let resolvedDevice = options.device;
35
+ if (typeof options.device === 'string') {
36
+ const lower = options.device.toLowerCase();
37
+ const matches = DecibriOutputBridge.devices().filter(d =>
38
+ d.name.toLowerCase().includes(lower)
39
+ );
40
+ if (matches.length === 0) {
41
+ throw new TypeError(`No audio output device found matching "${options.device}"`);
42
+ }
43
+ if (matches.length > 1) {
44
+ const names = matches.map(d => ` [${d.index}] ${d.name}`).join('\n');
45
+ throw new TypeError(
46
+ `Multiple devices match "${options.device}":\n${names}\nUse a more specific name or pass the device index directly.`
47
+ );
48
+ }
49
+ resolvedDevice = matches[0].index;
50
+ } else if (typeof options.device === 'number') {
51
+ const devices = DecibriOutputBridge.devices();
52
+ if (options.device < 0 || options.device >= devices.length) {
53
+ throw new RangeError('device index out of range — call DecibriOutput.devices() to list available devices');
54
+ }
55
+ resolvedDevice = options.device;
56
+ }
57
+
58
+ // ── Store config ───────────────────────────────────────────────────────
59
+
60
+ this._format = format;
61
+ this._started = false;
62
+
63
+ // ── Create native bridge ───────────────────────────────────────────────
64
+
65
+ this._native = new DecibriOutputBridge({
66
+ sampleRate,
67
+ channels,
68
+ format,
69
+ device: resolvedDevice,
70
+ });
71
+ }
72
+
73
+ /** @internal */
74
+ _write(chunk, encoding, callback) {
75
+ if (chunk.length === 0) {
76
+ callback();
77
+ return;
78
+ }
79
+ try {
80
+ this._native.write(chunk);
81
+ callback();
82
+ } catch (err) {
83
+ callback(err);
84
+ }
85
+ }
86
+
87
+ /** @internal Called by end() after all buffered writes complete. */
88
+ _final(callback) {
89
+ try {
90
+ this._native.drain();
91
+ callback();
92
+ } catch (err) {
93
+ callback(err);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Immediate stop. Discards remaining buffered audio.
99
+ */
100
+ stop() {
101
+ this._native.stop();
102
+ }
103
+
104
+ /**
105
+ * Whether audio is currently being output.
106
+ * @returns {boolean}
107
+ */
108
+ get isPlaying() {
109
+ return this._native.isPlaying;
110
+ }
111
+
112
+ /**
113
+ * List all available output devices on the system.
114
+ * @returns {Array<{index: number, name: string, maxOutputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
115
+ */
116
+ static devices() {
117
+ return DecibriOutputBridge.devices();
118
+ }
119
+
120
+ /**
121
+ * Version information for decibri and the audio runtime.
122
+ * @returns {{ decibri: string, portaudio: string }}
123
+ */
124
+ static version() {
125
+ return DecibriOutputBridge.version();
126
+ }
127
+ }
128
+
129
+ module.exports = DecibriOutput;
@@ -0,0 +1,237 @@
1
+ import { Readable, ReadableOptions, Writable, WritableOptions } from 'stream';
2
+
3
+ /** Information about an available audio input device. */
4
+ export interface DeviceInfo {
5
+ /** Device index (pass to constructor as `device`). */
6
+ index: number;
7
+ /** Human-readable device name from the OS. */
8
+ name: string;
9
+ /** Maximum number of input channels the device supports. */
10
+ maxInputChannels: number;
11
+ /** Device's preferred sample rate in Hz. */
12
+ defaultSampleRate: number;
13
+ /** Whether this is the current system default input device. */
14
+ isDefault: boolean;
15
+ }
16
+
17
+ /** Version strings returned by `Decibri.version()`. */
18
+ export interface VersionInfo {
19
+ /** decibri package version (e.g. `"3.0.0"`). */
20
+ decibri: string;
21
+ /** Audio runtime version string (e.g. `"cpal 0.17"`). */
22
+ portaudio: string;
23
+ }
24
+
25
+ /** Constructor options for `Decibri`. */
26
+ export interface DecibriOptions extends ReadableOptions {
27
+ /**
28
+ * Sample rate in Hz.
29
+ * @default 16000
30
+ * @range 1000–384000
31
+ */
32
+ sampleRate?: number;
33
+
34
+ /**
35
+ * Number of input channels.
36
+ * @default 1
37
+ * @range 1–32
38
+ */
39
+ channels?: number;
40
+
41
+ /**
42
+ * Frames per audio callback buffer. Controls chunk size and delivery interval.
43
+ * At 16 kHz mono, 1600 frames = 100 ms chunks of 3200 bytes (int16).
44
+ * @default 1600
45
+ * @range 64–65536
46
+ */
47
+ framesPerBuffer?: number;
48
+
49
+ /**
50
+ * Audio input device. Pass a numeric index or a case-insensitive name
51
+ * substring. Omit to use the system default input device.
52
+ */
53
+ device?: number | string;
54
+
55
+ /**
56
+ * Sample encoding format.
57
+ * - `'int16'`: 16-bit signed integer, little-endian (2 bytes per sample)
58
+ * - `'float32'`: 32-bit IEEE 754 float, little-endian (4 bytes per sample)
59
+ * @default 'int16'
60
+ */
61
+ format?: 'int16' | 'float32';
62
+
63
+ /**
64
+ * Enable energy-based voice activity detection.
65
+ * When enabled, emits `'speech'` and `'silence'` events.
66
+ * @default false
67
+ */
68
+ vad?: boolean;
69
+
70
+ /**
71
+ * RMS energy threshold for speech detection (VAD mode only).
72
+ * @default 0.01
73
+ * @range 0–1
74
+ */
75
+ vadThreshold?: number;
76
+
77
+ /**
78
+ * Milliseconds of sub-threshold audio before emitting `'silence'` (VAD mode only).
79
+ * @default 300
80
+ */
81
+ vadHoldoff?: number;
82
+
83
+ /**
84
+ * VAD engine to use.
85
+ * - `'energy'`: RMS energy threshold (default, lightweight)
86
+ * - `'silero'`: Silero VAD v5 ML model (more accurate, ~1ms inference)
87
+ * @default 'energy'
88
+ */
89
+ vadMode?: 'energy' | 'silero';
90
+
91
+ /**
92
+ * Path to the Silero VAD ONNX model file.
93
+ * Only used when `vadMode` is `'silero'`.
94
+ * Defaults to `models/silero_vad.onnx` relative to the package.
95
+ */
96
+ modelPath?: string;
97
+ }
98
+
99
+ /**
100
+ * Cross-platform microphone audio capture as a Node.js Readable stream.
101
+ *
102
+ * @example
103
+ * ```js
104
+ * const Decibri = require('decibri');
105
+ * const mic = new Decibri({ sampleRate: 16000, channels: 1 });
106
+ * mic.on('data', (chunk) => {
107
+ * // chunk is a Buffer of Int16 LE PCM samples
108
+ * });
109
+ * setTimeout(() => mic.stop(), 5000);
110
+ * ```
111
+ */
112
+ declare class Decibri extends Readable {
113
+ constructor(options?: DecibriOptions);
114
+
115
+ /** Stop microphone capture and end the stream. Safe to call multiple times. */
116
+ stop(): void;
117
+
118
+ /** Whether the microphone is currently capturing audio. */
119
+ readonly isOpen: boolean;
120
+
121
+ /** List all available audio input devices. */
122
+ static devices(): DeviceInfo[];
123
+
124
+ /** Version information for decibri and the audio runtime. */
125
+ static version(): VersionInfo;
126
+
127
+ // ── Event overloads ──────────────────────────────────────────────────────
128
+
129
+ on(event: 'data', listener: (chunk: Buffer) => void): this;
130
+ on(event: 'error', listener: (err: Error) => void): this;
131
+ on(event: 'end', listener: () => void): this;
132
+ on(event: 'close', listener: () => void): this;
133
+ on(event: 'pause', listener: () => void): this;
134
+ on(event: 'resume', listener: () => void): this;
135
+ on(event: 'readable', listener: () => void): this;
136
+ on(event: 'backpressure', listener: () => void): this;
137
+ on(event: 'speech', listener: () => void): this;
138
+ on(event: 'silence', listener: () => void): this;
139
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
140
+
141
+ once(event: 'data', listener: (chunk: Buffer) => void): this;
142
+ once(event: 'error', listener: (err: Error) => void): this;
143
+ once(event: 'end', listener: () => void): this;
144
+ once(event: 'close', listener: () => void): this;
145
+ once(event: 'backpressure', listener: () => void): this;
146
+ once(event: 'speech', listener: () => void): this;
147
+ once(event: 'silence', listener: () => void): this;
148
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
149
+ }
150
+
151
+ /** Information about an available audio output device. */
152
+ export interface OutputDeviceInfo {
153
+ /** Device index (pass to constructor as `device`). */
154
+ index: number;
155
+ /** Human-readable device name from the OS. */
156
+ name: string;
157
+ /** Maximum number of output channels the device supports. */
158
+ maxOutputChannels: number;
159
+ /** Device's preferred sample rate in Hz. */
160
+ defaultSampleRate: number;
161
+ /** Whether this is the current system default output device. */
162
+ isDefault: boolean;
163
+ }
164
+
165
+ /** Constructor options for `DecibriOutput`. */
166
+ export interface DecibriOutputOptions extends WritableOptions {
167
+ /**
168
+ * Sample rate in Hz.
169
+ * @default 16000
170
+ * @range 1000–384000
171
+ */
172
+ sampleRate?: number;
173
+
174
+ /**
175
+ * Number of output channels.
176
+ * @default 1
177
+ * @range 1–32
178
+ */
179
+ channels?: number;
180
+
181
+ /**
182
+ * Sample encoding format of incoming data.
183
+ * - `'int16'`: 16-bit signed integer, little-endian (2 bytes per sample)
184
+ * - `'float32'`: 32-bit IEEE 754 float, little-endian (4 bytes per sample)
185
+ * @default 'int16'
186
+ */
187
+ format?: 'int16' | 'float32';
188
+
189
+ /**
190
+ * Audio output device. Pass a numeric index or a case-insensitive name
191
+ * substring. Omit to use the system default output device.
192
+ */
193
+ device?: number | string;
194
+ }
195
+
196
+ /**
197
+ * Cross-platform audio output (speaker playback) as a Node.js Writable stream.
198
+ *
199
+ * @example
200
+ * ```js
201
+ * const { DecibriOutput } = require('decibri');
202
+ * const speaker = new DecibriOutput({ sampleRate: 16000, channels: 1 });
203
+ * speaker.write(pcmBuffer);
204
+ * speaker.end();
205
+ * ```
206
+ */
207
+ declare class DecibriOutput extends Writable {
208
+ constructor(options?: DecibriOutputOptions);
209
+
210
+ /** Immediate stop. Discards remaining buffered audio. */
211
+ stop(): void;
212
+
213
+ /** Whether audio is currently being output. */
214
+ readonly isPlaying: boolean;
215
+
216
+ /** List all available audio output devices. */
217
+ static devices(): OutputDeviceInfo[];
218
+
219
+ /** Version information for decibri and the audio runtime. */
220
+ static version(): VersionInfo;
221
+
222
+ // ── Event overloads ──────────────────────────────────────────────────────
223
+
224
+ on(event: 'drain', listener: () => void): this;
225
+ on(event: 'finish', listener: () => void): this;
226
+ on(event: 'error', listener: (err: Error) => void): this;
227
+ on(event: 'close', listener: () => void): this;
228
+ on(event: 'pipe', listener: (src: Readable) => void): this;
229
+ on(event: 'unpipe', listener: (src: Readable) => void): this;
230
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
231
+ }
232
+
233
+ export = Decibri;
234
+
235
+ declare namespace Decibri {
236
+ export { DecibriOutput };
237
+ }
package/src/decibri.js ADDED
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ const { Readable } = require('stream');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const { DecibriBridge } = require('../index.js');
7
+
8
+ // ─── RMS helper ──────────────────────────────────────────────────────────────
9
+
10
+ function computeRMS(chunk, format) {
11
+ let sum = 0, n;
12
+ if (format === 'float32') {
13
+ const samples = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4);
14
+ n = samples.length;
15
+ for (let i = 0; i < n; i++) sum += samples[i] * samples[i];
16
+ } else {
17
+ const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
18
+ n = samples.length;
19
+ for (let i = 0; i < n; i++) {
20
+ const s = samples[i] / 32768;
21
+ sum += s * s;
22
+ }
23
+ }
24
+ return n > 0 ? Math.sqrt(sum / n) : 0;
25
+ }
26
+
27
+ // ─── Decibri (Readable) ─────────────────────────────────────────────────────
28
+
29
+ class Decibri extends Readable {
30
+ /**
31
+ * @param {import('./decibri').DecibriOptions} [options]
32
+ */
33
+ constructor(options = {}) {
34
+ super({ highWaterMark: options.highWaterMark, objectMode: false });
35
+
36
+ // ── Validate options ───────────────────────────────────────────────────
37
+
38
+ const sampleRate = options.sampleRate ?? 16000;
39
+ if (sampleRate < 1000 || sampleRate > 384000) {
40
+ throw new RangeError('sampleRate must be between 1000 and 384000');
41
+ }
42
+
43
+ const channels = options.channels ?? 1;
44
+ if (channels < 1 || channels > 32) {
45
+ throw new RangeError('channels must be between 1 and 32');
46
+ }
47
+
48
+ const framesPerBuffer = options.framesPerBuffer ?? 1600;
49
+ if (framesPerBuffer < 64 || framesPerBuffer > 65536) {
50
+ throw new RangeError('framesPerBuffer must be between 64 and 65536');
51
+ }
52
+
53
+ const format = options.format ?? 'int16';
54
+ if (format !== 'int16' && format !== 'float32') {
55
+ throw new TypeError("format must be 'int16' or 'float32'");
56
+ }
57
+
58
+ // ── Resolve device ──────────────────────────────────────────────────────
59
+
60
+ let resolvedDevice = options.device;
61
+ if (typeof options.device === 'string') {
62
+ const lower = options.device.toLowerCase();
63
+ const matches = DecibriBridge.devices().filter(d =>
64
+ d.name.toLowerCase().includes(lower)
65
+ );
66
+ if (matches.length === 0) {
67
+ throw new TypeError(`No audio input device found matching "${options.device}"`);
68
+ }
69
+ if (matches.length > 1) {
70
+ const names = matches.map(d => ` [${d.index}] ${d.name}`).join('\n');
71
+ throw new TypeError(
72
+ `Multiple devices match "${options.device}":\n${names}\nUse a more specific name or pass the device index directly.`
73
+ );
74
+ }
75
+ resolvedDevice = matches[0].index;
76
+ } else if (typeof options.device === 'number') {
77
+ const devices = DecibriBridge.devices();
78
+ if (options.device < 0 || options.device >= devices.length) {
79
+ throw new RangeError('device index out of range — call Decibri.devices() to list available devices');
80
+ }
81
+ resolvedDevice = options.device;
82
+ }
83
+
84
+ // ── Validate VAD options ─────────────────────────────────────────────────
85
+
86
+ const vadMode = options.vadMode ?? 'energy';
87
+ if (vadMode !== 'energy' && vadMode !== 'silero') {
88
+ throw new TypeError("vadMode must be 'energy' or 'silero'");
89
+ }
90
+
91
+ let modelPath = undefined;
92
+ if (vadMode === 'silero' && options.vad) {
93
+ modelPath = options.modelPath || path.join(__dirname, '..', 'models', 'silero_vad.onnx');
94
+ if (!fs.existsSync(modelPath)) {
95
+ throw new Error(`Silero VAD model not found at ${modelPath}. Ensure the models/ directory is included in your installation.`);
96
+ }
97
+ }
98
+
99
+ // ── Store config ───────────────────────────────────────────────────────
100
+
101
+ this._format = format;
102
+ this._vad = options.vad || false;
103
+ this._vadMode = vadMode;
104
+ this._vadThreshold = options.vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01);
105
+ this._vadHoldoff = options.vadHoldoff ?? 300;
106
+ this._isSpeaking = false;
107
+ this._silenceTimer = null;
108
+ this._started = false;
109
+
110
+ // ── Create native bridge ───────────────────────────────────────────────
111
+
112
+ this._native = new DecibriBridge({
113
+ sampleRate,
114
+ channels,
115
+ framesPerBuffer,
116
+ format,
117
+ device: resolvedDevice,
118
+ vadMode: (options.vad && vadMode === 'silero') ? 'silero' : 'energy',
119
+ modelPath,
120
+ });
121
+ }
122
+
123
+ /** @internal */
124
+ _read() {
125
+ if (this._started) return;
126
+ this._started = true;
127
+
128
+ this._native.start((err, chunk) => {
129
+ if (err) {
130
+ this._started = false;
131
+ this.destroy(err);
132
+ return;
133
+ }
134
+
135
+ // push returns false when the consumer is slow. We can't pause a mic,
136
+ // but we surface the backpressure warning so callers can react.
137
+ if (!this.push(chunk)) {
138
+ this.emit('backpressure');
139
+ }
140
+
141
+ if (this._vad) {
142
+ if (this._vadMode === 'silero') {
143
+ const prob = this._native.vadProbability;
144
+ this._processVadSilero(prob);
145
+ } else {
146
+ this._processVadEnergy(chunk);
147
+ }
148
+ }
149
+ });
150
+ }
151
+
152
+ /** @internal Energy-based VAD (RMS threshold) */
153
+ _processVadEnergy(chunk) {
154
+ const rms = computeRMS(chunk, this._format);
155
+ this._processVadValue(rms);
156
+ }
157
+
158
+ /** @internal Silero ML-based VAD (reads probability from native) */
159
+ _processVadSilero(probability) {
160
+ this._processVadValue(probability);
161
+ }
162
+
163
+ /** @internal Common speech/silence state machine */
164
+ _processVadValue(value) {
165
+ if (value >= this._vadThreshold) {
166
+ clearTimeout(this._silenceTimer);
167
+ this._silenceTimer = null;
168
+ if (!this._isSpeaking) {
169
+ this._isSpeaking = true;
170
+ this.emit('speech');
171
+ }
172
+ } else if (this._isSpeaking && !this._silenceTimer) {
173
+ this._silenceTimer = setTimeout(() => {
174
+ this._isSpeaking = false;
175
+ this._silenceTimer = null;
176
+ this.emit('silence');
177
+ }, this._vadHoldoff);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Stop microphone capture and end the stream cleanly.
183
+ */
184
+ stop() {
185
+ if (!this._started) return;
186
+ this._started = false;
187
+ this._native.stop();
188
+ clearTimeout(this._silenceTimer);
189
+ this._silenceTimer = null;
190
+ this.push(null); // signals stream end
191
+ }
192
+
193
+ /**
194
+ * Whether the microphone is currently open.
195
+ * @returns {boolean}
196
+ */
197
+ get isOpen() {
198
+ return this._native.isOpen;
199
+ }
200
+
201
+ /**
202
+ * List all available input devices on the system.
203
+ * @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
204
+ */
205
+ static devices() {
206
+ return DecibriBridge.devices();
207
+ }
208
+
209
+ /**
210
+ * Version information for decibri and the audio runtime.
211
+ * @returns {{ decibri: string, portaudio: string }}
212
+ */
213
+ static version() {
214
+ return DecibriBridge.version();
215
+ }
216
+ }
217
+
218
+ const DecibriOutput = require('./decibri-output.js');
219
+ Decibri.DecibriOutput = DecibriOutput;
220
+
221
+ module.exports = Decibri;