voice-amd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+
3
+ const { EventEmitter } = require("events");
4
+ const {
5
+ DEFAULT_CONFIG,
6
+ AMD_DECISIONS,
7
+ AUDIO_FORMATS,
8
+ } = require("./constants/defaults");
9
+ const PcmUtils = require("./audio/pcmUtils");
10
+ const VoiceActivityDetector = require("./audio/vad");
11
+ const GoertzelDetector = require("./dsp/goertzel");
12
+ const AnsweringMachineStateMachine = require("./classifier/stateMachine");
13
+ const TextPatternClassifier = require("./classifier/textPatterns");
14
+
15
+ class AnsweringMachineDetector extends EventEmitter {
16
+ /**
17
+ * @param {Object} options Configuration overrides
18
+ */
19
+ constructor(options = {}) {
20
+ super();
21
+
22
+ this.options = Object.assign({}, DEFAULT_CONFIG, options);
23
+
24
+ // Audio format & native sample rate resolution
25
+ this.audioFormat = this.options.audioFormat || DEFAULT_CONFIG.audioFormat;
26
+
27
+ if (options.sampleRate) {
28
+ this.sampleRate = options.sampleRate;
29
+ } else if (
30
+ this.audioFormat === AUDIO_FORMATS.ULAW_8K ||
31
+ this.audioFormat === AUDIO_FORMATS.ALAW_8K ||
32
+ this.audioFormat === AUDIO_FORMATS.PCM_8K
33
+ ) {
34
+ this.sampleRate = 8000;
35
+ } else if (this.audioFormat === AUDIO_FORMATS.PCM_24K) {
36
+ this.sampleRate = 24000;
37
+ } else if (this.audioFormat === AUDIO_FORMATS.PCM_32K) {
38
+ this.sampleRate = 32000;
39
+ } else if (this.audioFormat === AUDIO_FORMATS.PCM_44K) {
40
+ this.sampleRate = 44100;
41
+ } else if (this.audioFormat === AUDIO_FORMATS.PCM_48K) {
42
+ this.sampleRate = 48000;
43
+ } else {
44
+ this.sampleRate = 16000;
45
+ }
46
+
47
+ this.frameSizeMs = this.options.frameSizeMs || 20;
48
+
49
+ // Native frame calculations (Zero Resampling!)
50
+ // 16-bit linear PCM has 2 bytes per sample
51
+ this.bytesPerMs = (this.sampleRate * 2) / 1000;
52
+ this.bytesPerFrame = this.bytesPerMs * this.frameSizeMs;
53
+
54
+ // Submodules (configured for native sample rate)
55
+ const subOptions = Object.assign({}, this.options, {
56
+ sampleRate: this.sampleRate,
57
+ });
58
+ this.vad = new VoiceActivityDetector(subOptions);
59
+ this.stateMachine = new AnsweringMachineStateMachine(subOptions);
60
+
61
+ if (this.options.enableBeepDetection) {
62
+ this.beepDetector = new GoertzelDetector(subOptions);
63
+ } else {
64
+ this.beepDetector = null;
65
+ }
66
+
67
+ // Residual audio buffer for frame alignment
68
+ this.audioBuffer = Buffer.alloc(0);
69
+
70
+ // Activity flags
71
+ this.wasSpeaking = false;
72
+ this.currentSpeechDurationMs = 0;
73
+ this.hasDecided = false;
74
+ this.lastDecision = null;
75
+ }
76
+
77
+ /**
78
+ * Process a single incoming chunk of audio (Buffer or base64 string)
79
+ * Natively processes at the audio's original sample rate with zero resampling.
80
+ *
81
+ * @param {Buffer|string} chunk
82
+ * @returns {Object} Current decision status
83
+ */
84
+ processChunk(chunk) {
85
+ if (this.hasDecided) {
86
+ return this.stateMachine.getResult();
87
+ }
88
+
89
+ if (!chunk) return this.stateMachine.getResult();
90
+
91
+ // 1. Decode non-linear codecs to 16-bit PCM at native sample rate (Zero Resampling)
92
+ const nativePcm16 = PcmUtils.decodeToNativePcm16(chunk, {
93
+ audioFormat: this.audioFormat,
94
+ channels: this.options.channels,
95
+ endianness: this.options.endianness,
96
+ });
97
+
98
+ // 2. Append to accumulator buffer
99
+ this.audioBuffer = Buffer.concat([this.audioBuffer, nativePcm16]);
100
+
101
+ // 3. Process complete fixed-size native frames (e.g. 20ms at native rate)
102
+ while (this.audioBuffer.length >= this.bytesPerFrame && !this.hasDecided) {
103
+ const frame = this.audioBuffer.subarray(0, this.bytesPerFrame);
104
+ this.audioBuffer = this.audioBuffer.subarray(this.bytesPerFrame);
105
+
106
+ this._processFrame(frame);
107
+ }
108
+
109
+ return this.stateMachine.getResult();
110
+ }
111
+
112
+ /**
113
+ * Process a single aligned native audio frame
114
+ * @private
115
+ * @param {Buffer} frame - Native 20ms 16-bit linear PCM frame
116
+ */
117
+ _processFrame(frame) {
118
+ const frameMs = this.frameSizeMs;
119
+
120
+ // 1. Run Tone / Beep Detector at native sample rate
121
+ if (this.beepDetector) {
122
+ const beepResult = this.beepDetector.process(frame, frameMs);
123
+ if (beepResult.isBeep) {
124
+ const result = this.stateMachine.forceDecision(
125
+ AMD_DECISIONS.BEEP,
126
+ `Voicemail beep tone detected (${beepResult.detectedFrequency}Hz)`,
127
+ 0.99,
128
+ );
129
+ this._finalizeDecision(result, {
130
+ frequency: beepResult.detectedFrequency,
131
+ });
132
+ return;
133
+ }
134
+ }
135
+
136
+ // 2. Run Voice Activity Detector at native sample rate
137
+ const vadResult = this.vad.analyze(frame);
138
+
139
+ // 3. Emit granular speech transition events
140
+ if (vadResult.isSpeaking && !this.wasSpeaking) {
141
+ this.wasSpeaking = true;
142
+ this.currentSpeechDurationMs = 0;
143
+ this.emit("speech_start", {
144
+ rms: vadResult.rms,
145
+ snr: vadResult.snr,
146
+ zcrFrequencyHz: vadResult.zcrFrequencyHz,
147
+ timestamp: Date.now(),
148
+ });
149
+ } else if (!vadResult.isSpeaking && this.wasSpeaking) {
150
+ this.wasSpeaking = false;
151
+ this.emit("speech_end", {
152
+ durationMs: this.currentSpeechDurationMs,
153
+ timestamp: Date.now(),
154
+ });
155
+ }
156
+
157
+ if (vadResult.isSpeaking) {
158
+ this.currentSpeechDurationMs += frameMs;
159
+ }
160
+
161
+ // 4. Update State Machine
162
+ const smResult = this.stateMachine.update(vadResult.isSpeaking, frameMs);
163
+
164
+ if (smResult.isFinal) {
165
+ this._finalizeDecision(smResult);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Process an optional text transcript snippet (e.g. from an ASR/STT provider)
171
+ * to immediately detect carrier IVR phrases.
172
+ * @param {string} text
173
+ * @returns {Object|null}
174
+ */
175
+ processTranscript(text) {
176
+ if (this.hasDecided || !text) return this.stateMachine.getResult();
177
+
178
+ const classification = TextPatternClassifier.classify(text);
179
+ if (classification.isVoicemail) {
180
+ const result = this.stateMachine.forceDecision(
181
+ AMD_DECISIONS.VOICEMAIL,
182
+ `Carrier IVR phrase detected: "${classification.matchedPattern}" (${classification.label})`,
183
+ classification.confidence,
184
+ );
185
+ this._finalizeDecision(result, {
186
+ transcriptMatch: classification.matchedPattern,
187
+ label: classification.label,
188
+ });
189
+ return result;
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Finalize the decision, emit events, and freeze processing
197
+ * @private
198
+ * @param {Object} result
199
+ * @param {Object} extraDetails
200
+ */
201
+ _finalizeDecision(result, extraDetails = {}) {
202
+ if (this.hasDecided) return;
203
+
204
+ this.hasDecided = true;
205
+ this.lastDecision = Object.assign({}, result, { extra: extraDetails });
206
+
207
+ // Emit generic decision event
208
+ this.emit("decision", {
209
+ type: result.decision,
210
+ confidence: result.confidence,
211
+ reason: result.reason,
212
+ details: result.details,
213
+ extra: extraDetails,
214
+ });
215
+
216
+ // Emit specific convenience events
217
+ switch (result.decision) {
218
+ case AMD_DECISIONS.HUMAN:
219
+ this.emit("human", {
220
+ confidence: result.confidence,
221
+ reason: result.reason,
222
+ details: result.details,
223
+ });
224
+ break;
225
+
226
+ case AMD_DECISIONS.VOICEMAIL:
227
+ this.emit("voicemail", {
228
+ confidence: result.confidence,
229
+ reason: result.reason,
230
+ details: result.details,
231
+ extra: extraDetails,
232
+ });
233
+ break;
234
+
235
+ case AMD_DECISIONS.BEEP:
236
+ this.emit("beep", {
237
+ confidence: result.confidence,
238
+ frequency: extraDetails.frequency,
239
+ reason: result.reason,
240
+ details: result.details,
241
+ });
242
+ break;
243
+
244
+ case AMD_DECISIONS.SILENCE:
245
+ this.emit("silence", {
246
+ confidence: result.confidence,
247
+ reason: result.reason,
248
+ details: result.details,
249
+ });
250
+ break;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Node.js Stream / Transform write alias
256
+ * @param {Buffer|string} chunk
257
+ */
258
+ write(chunk) {
259
+ this.processChunk(chunk);
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * Node.js Stream end alias
265
+ */
266
+ end() {
267
+ if (!this.hasDecided) {
268
+ const result = this.stateMachine.update(
269
+ false,
270
+ this.options.maxAnalysisWindowMs,
271
+ );
272
+ if (result.isFinal) {
273
+ this._finalizeDecision(result);
274
+ }
275
+ }
276
+ this.emit("finish");
277
+ }
278
+
279
+ /**
280
+ * Retrieve current decision state
281
+ * @returns {Object}
282
+ */
283
+ getDecision() {
284
+ return this.stateMachine.getResult();
285
+ }
286
+
287
+ /**
288
+ * Reset detector for a new call
289
+ */
290
+ reset() {
291
+ this.audioBuffer = Buffer.alloc(0);
292
+ this.wasSpeaking = false;
293
+ this.currentSpeechDurationMs = 0;
294
+ this.hasDecided = false;
295
+ this.lastDecision = null;
296
+ this.vad.reset();
297
+ this.stateMachine.reset();
298
+ if (this.beepDetector) this.beepDetector.reset();
299
+ }
300
+ }
301
+
302
+ module.exports = AnsweringMachineDetector;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+
3
+ const { AUDIO_FORMATS } = require("../constants/defaults");
4
+
5
+ // Precomputed µ-law expansion table for 16-bit linear PCM (G.711u)
6
+ const ULAW_TO_PCM16_TABLE = new Int16Array(256);
7
+ for (let i = 0; i < 256; i++) {
8
+ let u = ~i;
9
+ let sign = u & 0x80;
10
+ let exponent = (u >> 4) & 0x07;
11
+ let mantissa = u & 0x0f;
12
+ let sample = ((mantissa << 3) + 0x84) << exponent;
13
+ sample -= 0x84;
14
+ ULAW_TO_PCM16_TABLE[i] = sign ? -sample : sample;
15
+ }
16
+
17
+ // Precomputed A-law expansion table for 16-bit linear PCM (G.711a)
18
+ const ALAW_TO_PCM16_TABLE = new Int16Array(256);
19
+ for (let i = 0; i < 256; i++) {
20
+ let a = i ^ 0x55;
21
+ let sign = a & 0x80;
22
+ let exponent = (a >> 4) & 0x07;
23
+ let mantissa = a & 0x0f;
24
+ let sample = 0;
25
+ if (exponent === 0) {
26
+ sample = (mantissa << 4) + 8;
27
+ } else {
28
+ sample = ((mantissa << 4) + 0x108) << (exponent - 1);
29
+ }
30
+ ALAW_TO_PCM16_TABLE[i] = sign ? -sample : sample;
31
+ }
32
+
33
+ class PcmUtils {
34
+ /**
35
+ * Ensure input is a Node.js Buffer
36
+ * @param {Buffer|string} input
37
+ * @returns {Buffer}
38
+ */
39
+ static toBuffer(input) {
40
+ if (Buffer.isBuffer(input)) return input;
41
+ if (typeof input === "string") return Buffer.from(input, "base64");
42
+ throw new TypeError("Audio input must be a Buffer or base64 string");
43
+ }
44
+
45
+ /**
46
+ * Convert 8kHz µ-law (G.711u) buffer to 16-bit linear PCM (keeps native 8kHz)
47
+ * @param {Buffer} ulawBuf
48
+ * @returns {Buffer}
49
+ */
50
+ static ulawToPcm16(ulawBuf) {
51
+ const pcmBuf = Buffer.alloc(ulawBuf.length * 2);
52
+ for (let i = 0; i < ulawBuf.length; i++) {
53
+ const sample = ULAW_TO_PCM16_TABLE[ulawBuf[i]];
54
+ pcmBuf.writeInt16LE(sample, i * 2);
55
+ }
56
+ return pcmBuf;
57
+ }
58
+
59
+ /**
60
+ * Convert 8kHz A-law (G.711a) buffer to 16-bit linear PCM (keeps native 8kHz)
61
+ * @param {Buffer} alawBuf
62
+ * @returns {Buffer}
63
+ */
64
+ static alawToPcm16(alawBuf) {
65
+ const pcmBuf = Buffer.alloc(alawBuf.length * 2);
66
+ for (let i = 0; i < alawBuf.length; i++) {
67
+ const sample = ALAW_TO_PCM16_TABLE[alawBuf[i]];
68
+ pcmBuf.writeInt16LE(sample, i * 2);
69
+ }
70
+ return pcmBuf;
71
+ }
72
+
73
+ /**
74
+ * Convert 32-bit Float PCM (-1.0 to 1.0) to 16-bit linear PCM (keeps native sample rate)
75
+ * @param {Buffer} floatBuf
76
+ * @param {string} endianness - "LE" | "BE"
77
+ * @returns {Buffer}
78
+ */
79
+ static float32ToPcm16(floatBuf, endianness = "LE") {
80
+ const numSamples = Math.floor(floatBuf.length / 4);
81
+ const pcmBuf = Buffer.alloc(numSamples * 2);
82
+
83
+ for (let i = 0; i < numSamples; i++) {
84
+ const floatVal =
85
+ endianness === "BE"
86
+ ? floatBuf.readFloatBE(i * 4)
87
+ : floatBuf.readFloatLE(i * 4);
88
+
89
+ const clamped = Math.max(-1.0, Math.min(1.0, floatVal));
90
+ const int16 =
91
+ clamped < 0 ? Math.round(clamped * 32768) : Math.round(clamped * 32767);
92
+
93
+ pcmBuf.writeInt16LE(int16, i * 2);
94
+ }
95
+
96
+ return pcmBuf;
97
+ }
98
+
99
+ /**
100
+ * Convert 8-bit unsigned PCM (0-255) to 16-bit linear PCM (keeps native sample rate)
101
+ * @param {Buffer} pcm8Buf
102
+ * @returns {Buffer}
103
+ */
104
+ static pcm8bitToPcm16(pcm8Buf) {
105
+ const pcmBuf = Buffer.alloc(pcm8Buf.length * 2);
106
+ for (let i = 0; i < pcm8Buf.length; i++) {
107
+ const sample = (pcm8Buf[i] - 128) << 8;
108
+ pcmBuf.writeInt16LE(sample, i * 2);
109
+ }
110
+ return pcmBuf;
111
+ }
112
+
113
+ /**
114
+ * Downmix 2-channel Stereo 16-bit PCM to Mono (keeps native sample rate)
115
+ * @param {Buffer} stereoBuf
116
+ * @returns {Buffer}
117
+ */
118
+ static downmixStereoToMono(stereoBuf) {
119
+ const numFrames = Math.floor(stereoBuf.length / 4);
120
+ const monoBuf = Buffer.alloc(numFrames * 2);
121
+
122
+ for (let i = 0; i < numFrames; i++) {
123
+ const left = stereoBuf.readInt16LE(i * 4);
124
+ const right = stereoBuf.readInt16LE(i * 4 + 2);
125
+ const mixed = Math.round((left + right) / 2);
126
+ monoBuf.writeInt16LE(mixed, i * 2);
127
+ }
128
+
129
+ return monoBuf;
130
+ }
131
+
132
+ /**
133
+ * Convert any audio format to 16-bit linear PCM at its NATIVE sample rate (Zero-Resampling)
134
+ * @param {Buffer|string} input - Audio data
135
+ * @param {Object|string} options - Format string or options { audioFormat, channels, endianness }
136
+ * @returns {Buffer} 16-bit linear PCM at native sample rate
137
+ */
138
+ static decodeToNativePcm16(input, options = {}) {
139
+ let buf = this.toBuffer(input);
140
+
141
+ let format = AUDIO_FORMATS.PCM_16K;
142
+ let channels = 1;
143
+ let endianness = "LE";
144
+
145
+ if (typeof options === "string") {
146
+ format = options;
147
+ } else if (typeof options === "object") {
148
+ format = options.audioFormat || format;
149
+ channels = options.channels || channels;
150
+ endianness = options.endianness || endianness;
151
+ }
152
+
153
+ let pcm16;
154
+ switch (format) {
155
+ case AUDIO_FORMATS.ULAW_8K:
156
+ pcm16 = this.ulawToPcm16(buf);
157
+ break;
158
+
159
+ case AUDIO_FORMATS.ALAW_8K:
160
+ pcm16 = this.alawToPcm16(buf);
161
+ break;
162
+
163
+ case AUDIO_FORMATS.FLOAT_32:
164
+ pcm16 = this.float32ToPcm16(buf, endianness);
165
+ break;
166
+
167
+ case AUDIO_FORMATS.PCM_8BIT:
168
+ pcm16 = this.pcm8bitToPcm16(buf);
169
+ break;
170
+
171
+ default:
172
+ pcm16 = buf;
173
+ break;
174
+ }
175
+
176
+ if (channels === 2) {
177
+ pcm16 = this.downmixStereoToMono(pcm16);
178
+ }
179
+
180
+ return pcm16;
181
+ }
182
+
183
+ /**
184
+ * Extract Int16Array of samples from a 16-bit PCM Buffer
185
+ * @param {Buffer} pcmBuf
186
+ * @returns {Int16Array}
187
+ */
188
+ static pcmToInt16Array(pcmBuf) {
189
+ const sampleCount = Math.floor(pcmBuf.length / 2);
190
+ const samples = new Int16Array(sampleCount);
191
+ for (let i = 0; i < sampleCount; i++) {
192
+ samples[i] = pcmBuf.readInt16LE(i * 2);
193
+ }
194
+ return samples;
195
+ }
196
+ }
197
+
198
+ module.exports = PcmUtils;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+
3
+ const { DEFAULT_CONFIG } = require("../constants/defaults");
4
+ const PcmUtils = require("./pcmUtils");
5
+
6
+ class VoiceActivityDetector {
7
+ /**
8
+ * @param {Object} options
9
+ */
10
+ constructor(options = {}) {
11
+ this.sampleRate = options.sampleRate || DEFAULT_CONFIG.sampleRate;
12
+ this.speechThresholdRMS =
13
+ options.speechThresholdRMS || DEFAULT_CONFIG.speechThresholdRMS;
14
+ this.silenceThresholdRMS =
15
+ options.silenceThresholdRMS || DEFAULT_CONFIG.silenceThresholdRMS;
16
+ this.adaptiveNoiseFloor =
17
+ options.adaptiveNoiseFloor !== undefined
18
+ ? options.adaptiveNoiseFloor
19
+ : DEFAULT_CONFIG.adaptiveNoiseFloor;
20
+
21
+ // Noise floor initialized to a clean baseline (80)
22
+ this.noiseFloor = this.silenceThresholdRMS;
23
+ this.noiseAlpha = 0.08;
24
+
25
+ // Hangover smoothing to bridge consonant-vowel transitions (40ms)
26
+ this.hangoverFrames = 0;
27
+ this.maxHangoverFrames = 2;
28
+ }
29
+
30
+ /**
31
+ * Analyze a 16-bit linear PCM audio chunk at its native sample rate
32
+ * @param {Buffer|Int16Array} chunk - 16-bit linear PCM chunk
33
+ * @returns {Object} { isSpeaking, rms, zcr, zcrFrequencyHz, snr, noiseFloor }
34
+ */
35
+ analyze(chunk) {
36
+ const samples = Buffer.isBuffer(chunk)
37
+ ? PcmUtils.pcmToInt16Array(chunk)
38
+ : chunk;
39
+
40
+ const N = samples.length;
41
+ if (N === 0) {
42
+ return {
43
+ isSpeaking: false,
44
+ rms: 0,
45
+ zcr: 0,
46
+ zcrFrequencyHz: 0,
47
+ snr: 0,
48
+ noiseFloor: this.noiseFloor,
49
+ };
50
+ }
51
+
52
+ // 1. Calculate DC Bias / Mean of the Frame
53
+ let sum = 0;
54
+ for (let i = 0; i < N; i++) {
55
+ sum += samples[i];
56
+ }
57
+ const mean = sum / N;
58
+
59
+ // 2. AC-Coupled RMS Energy and Zero-Crossing Rate (removes hardware DC bias)
60
+ let sumSquares = 0;
61
+ let zeroCrossings = 0;
62
+ let prevSample = 0;
63
+
64
+ for (let i = 0; i < N; i++) {
65
+ const acSample = samples[i] - mean;
66
+ sumSquares += acSample * acSample;
67
+
68
+ if (i > 0) {
69
+ if (
70
+ (acSample >= 0 && prevSample < 0) ||
71
+ (acSample < 0 && prevSample >= 0)
72
+ ) {
73
+ zeroCrossings++;
74
+ }
75
+ }
76
+ prevSample = acSample;
77
+ }
78
+
79
+ const rms = Math.sqrt(sumSquares / N);
80
+ const zcr = parseFloat((zeroCrossings / N).toFixed(4));
81
+ // Physical zero-crossing frequency in Hz
82
+ const zcrFrequencyHz = Math.round(
83
+ (zeroCrossings * this.sampleRate) / (2 * N),
84
+ );
85
+
86
+ // 3. Adaptive Noise Floor Tracking
87
+ if (this.adaptiveNoiseFloor) {
88
+ if (rms < this.speechThresholdRMS * 0.7) {
89
+ this.noiseFloor =
90
+ this.noiseFloor * (1 - this.noiseAlpha) + rms * this.noiseAlpha;
91
+ }
92
+ }
93
+
94
+ // Signal-to-Noise Ratio (SNR) in dB
95
+ const snr =
96
+ this.noiseFloor > 0
97
+ ? 20 * Math.log10((rms + 1e-5) / Math.max(1, this.noiseFloor))
98
+ : 0;
99
+
100
+ // 4. Multi-Feature Speech Decision
101
+ const isHumanSpectralRange =
102
+ zcrFrequencyHz >= 50 && zcrFrequencyHz <= 6000;
103
+
104
+ // High energy speech (vowels)
105
+ const isHighEnergy = rms >= this.speechThresholdRMS && isHumanSpectralRange;
106
+ // Distinct voice with solid SNR above ambient noise floor
107
+ const isDistinctVoice = rms >= 140 && snr >= 4.0 && isHumanSpectralRange;
108
+
109
+ let isRawSpeaking = isHighEnergy || isDistinctVoice;
110
+
111
+ // 5. Hangover smoothing for natural speech continuation
112
+ if (isRawSpeaking) {
113
+ this.hangoverFrames = this.maxHangoverFrames;
114
+ } else if (this.hangoverFrames > 0) {
115
+ this.hangoverFrames--;
116
+ isRawSpeaking = true;
117
+ }
118
+
119
+ return {
120
+ isSpeaking: isRawSpeaking,
121
+ rms: Math.round(rms),
122
+ zcr,
123
+ zcrFrequencyHz,
124
+ snr: parseFloat(snr.toFixed(2)),
125
+ noiseFloor: Math.round(this.noiseFloor),
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Reset noise floor tracking and hangover
131
+ */
132
+ reset() {
133
+ this.noiseFloor = this.silenceThresholdRMS;
134
+ this.hangoverFrames = 0;
135
+ }
136
+ }
137
+
138
+ module.exports = VoiceActivityDetector;