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.
@@ -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;