Ver código fonte

Noisy microphone detection (#1013)

* feat: implement vad noise detection

* start detection services on initial track add

* additional refactor / calibrate noise detection

* stop detection services on track remove

* address code review

* address code review p2
dev1
Andrei Gavrilescu 5 anos atrás
pai
commit
e4b523d0fa

+ 31
- 2
JitsiConference.js Ver arquivo

@@ -17,6 +17,8 @@ import P2PDominantSpeakerDetection from './modules/detection/P2PDominantSpeakerD
17 17
 import RTC from './modules/RTC/RTC';
18 18
 import TalkMutedDetection from './modules/detection/TalkMutedDetection';
19 19
 import VADTalkMutedDetection from './modules/detection/VADTalkMutedDetection';
20
+import VADNoiseDetection from './modules/detection/VADNoiseDetection';
21
+import VADAudioAnalyser from './modules/detection/VADAudioAnalyser';
20 22
 import * as DetectionEvents from './modules/detection/DetectionEvents';
21 23
 import NoAudioSignalDetection from './modules/detection/NoAudioSignalDetection';
22 24
 import browser from './modules/browser';
@@ -375,14 +377,24 @@ JitsiConference.prototype._init = function(options = {}) {
375 377
     this.eventManager.setupStatisticsListeners();
376 378
 
377 379
     if (config.enableTalkWhileMuted) {
380
+
378 381
         // If VAD processor factory method is provided uses VAD based detection, otherwise fallback to audio level
379 382
         // based detection.
380 383
         if (config.createVADProcessor) {
381 384
             logger.info('Using VAD detection for generating talk while muted events');
382
-            this._talkWhileMutedDetection = new VADTalkMutedDetection(this, config.createVADProcessor);
383
-            this._talkWhileMutedDetection.on(DetectionEvents.VAD_TALK_WHILE_MUTED, () =>
385
+
386
+            if (!this._audioAnalyser) {
387
+                this._audioAnalyser = new VADAudioAnalyser(this, config.createVADProcessor);
388
+            }
389
+
390
+            const vadTalkMutedDetection = new VADTalkMutedDetection();
391
+
392
+            vadTalkMutedDetection.on(DetectionEvents.VAD_TALK_WHILE_MUTED, () =>
384 393
                 this.eventEmitter.emit(JitsiConferenceEvents.TALK_WHILE_MUTED));
385 394
 
395
+            this._audioAnalyser.addVADDetectionService(vadTalkMutedDetection);
396
+
397
+
386 398
         } else {
387 399
             logger.info('Using audio level based detection for generating talk while muted events');
388 400
             this._talkWhileMutedDetection = new TalkMutedDetection(
@@ -390,6 +402,23 @@ JitsiConference.prototype._init = function(options = {}) {
390 402
         }
391 403
     }
392 404
 
405
+    if (config.enableNoisyMicDetection) {
406
+        if (config.createVADProcessor) {
407
+            if (!this._audioAnalyser) {
408
+                this._audioAnalyser = new VADAudioAnalyser(this, config.createVADProcessor);
409
+            }
410
+
411
+            const vadNoiseDetection = new VADNoiseDetection();
412
+
413
+            vadNoiseDetection.on(DetectionEvents.VAD_NOISY_DEVICE, () =>
414
+                this.eventEmitter.emit(JitsiConferenceEvents.NOISY_MIC));
415
+
416
+            this._audioAnalyser.addVADDetectionService(vadNoiseDetection);
417
+        } else {
418
+            logger.warn('No VAD Processor was provided. Noisy microphone detection service was not initialized!');
419
+        }
420
+    }
421
+
393 422
     // Generates events based on no audio input detector.
394 423
     if (config.enableNoAudioDetection) {
395 424
         this._noAudioSignalDetection = new NoAudioSignalDetection(this);

+ 16
- 11
JitsiConferenceEvents.js Ver arquivo

@@ -2,6 +2,12 @@
2 2
  * The events for the conference.
3 3
  */
4 4
 
5
+/**
6
+ * Event indicates that the current conference audio input switched between audio
7
+ * input states,i.e. with or without audio input.
8
+ */
9
+export const AUDIO_INPUT_STATE_CHANGE = 'conference.audio_input_state_changed';
10
+
5 11
 /**
6 12
  * Indicates that authentication status changed.
7 13
  */
@@ -147,6 +153,16 @@ export const SERVER_REGION_CHANGED = 'conference.server_region_changed';
147 153
  */
148 154
 export const MESSAGE_RECEIVED = 'conference.messageReceived';
149 155
 
156
+/**
157
+ * Event indicates that the current selected input device has no signal
158
+ */
159
+export const NO_AUDIO_INPUT = 'conference.no_audio_input';
160
+
161
+/**
162
+ * Event indicates that the current microphone used by the conference is noisy.
163
+ */
164
+export const NOISY_MIC = 'conference.noisy_mic';
165
+
150 166
 /**
151 167
  * New private text message was received.
152 168
  */
@@ -250,17 +266,6 @@ export const SUSPEND_DETECTED = 'conference.suspendDetected';
250 266
  */
251 267
 export const TALK_WHILE_MUTED = 'conference.talk_while_muted';
252 268
 
253
-/**
254
- * Event indicates that the current selected input device has no signal
255
- */
256
-export const NO_AUDIO_INPUT = 'conference.no_audio_input';
257
-
258
-/**
259
- * Event indicates that the current conference audio input switched between audio
260
- * input states,i.e. with or without audio input.
261
- */
262
-export const AUDIO_INPUT_STATE_CHANGE = 'conference.audio_input_state_changed';
263
-
264 269
 /**
265 270
  * A new media track was added to the conference. The event provides the
266 271
  * following parameters to its listeners:

+ 1
- 0
doc/API.md Ver arquivo

@@ -137,6 +137,7 @@ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
137 137
         - TALK_WHILE_MUTED - notifies that a local user is talking while having the microphone muted.
138 138
         - NO_AUDIO_INPUT - notifies that the current selected input device has no signal.
139 139
         - AUDIO_INPUT_STATE_CHANGE - notifies that the current conference audio input switched between audio input states i.e. with or without audio input.
140
+        - NOISY_MIC - notifies that the current microphone used by the conference is noisy.
140 141
 
141 142
     2. connection
142 143
         - CONNECTION_FAILED - indicates that the server connection failed.

+ 20
- 4
modules/detection/DetectionEvents.js Ver arquivo

@@ -1,9 +1,10 @@
1
-/** Event triggered by NoAudioSignalDetector when the local audio device associated with a JitsiConference goes silent
2
- * for a period of time, meaning that the device is either broken or hardware/software muted.
1
+/**
2
+ * Event triggered by a audio detector indicating that its active state has changed from active to inactive or vice
3
+ * versa.
3 4
  * @event
4
- * @type {void}
5
+ * @type {boolean} - true when service has changed to active false otherwise.
5 6
  */
6
-export const NO_AUDIO_INPUT = 'no_audio_input_detected';
7
+export const DETECTOR_STATE_CHANGE = 'detector_state_change';
7 8
 
8 9
 /** Event triggered by {@link NoAudioSignalDetector} when the local audio device associated with a JitsiConference
9 10
  * starts receiving audio levels with the value of 0 meaning no audio is being captured on that device, or when
@@ -13,6 +14,20 @@ export const NO_AUDIO_INPUT = 'no_audio_input_detected';
13 14
  */
14 15
 export const AUDIO_INPUT_STATE_CHANGE = 'audio_input_state_changed';
15 16
 
17
+/** Event triggered by NoAudioSignalDetector when the local audio device associated with a JitsiConference goes silent
18
+ * for a period of time, meaning that the device is either broken or hardware/software muted.
19
+ * @event
20
+ * @type {void}
21
+ */
22
+export const NO_AUDIO_INPUT = 'no_audio_input_detected';
23
+
24
+/**
25
+ *  Event generated by {@link VADNoiseDetection} when the tracked device is considered noisy.
26
+ *  @event
27
+ *  @type {Object}
28
+ */
29
+export const VAD_NOISY_DEVICE = 'detection.vad_noise_device';
30
+
16 31
 /**
17 32
  * Event generated by VADReportingService when if finishes creating a VAD report for the monitored devices.
18 33
  * The generated objects are of type Array<Object>, one score for each monitored device.
@@ -31,6 +46,7 @@ export const VAD_REPORT_PUBLISHED = 'vad-report-published';
31 46
  * @type {Object}
32 47
  * @property {Date}   timestamp - Exact time at which processed PCM sample was generated.
33 48
  * @property {number} score - VAD score on a scale from 0 to 1 (i.e. 0.7)
49
+ * @property {Float32Array} pcmData - Raw PCM data with which the VAD score was calculated.
34 50
  * @property {string} deviceId - Device id of the associated track.
35 51
  */
36 52
 export const VAD_SCORE_PUBLISHED = 'detection.vad_score_published';

+ 7
- 3
modules/detection/TrackVADEmitter.js Ver arquivo

@@ -2,6 +2,7 @@ import EventEmitter from 'events';
2 2
 
3 3
 import RTC from '../RTC/RTC';
4 4
 
5
+import { createAudioContext } from './webaudio/WebAudioUtils';
5 6
 import { VAD_SCORE_PUBLISHED } from './DetectionEvents';
6 7
 
7 8
 /**
@@ -48,7 +49,7 @@ export default class TrackVADEmitter extends EventEmitter {
48 49
         /**
49 50
          * The AudioContext instance with the preferred sample frequency.
50 51
          */
51
-        this._audioContext = new AudioContext({ sampleRate: vadProcessor.getRequiredPCMFrequency() });
52
+        this._audioContext = createAudioContext({ sampleRate: vadProcessor.getRequiredPCMFrequency() });
52 53
 
53 54
         /**
54 55
          * PCM Sample size expected by the VAD Processor instance. We cache it here as this value is used extensively,
@@ -97,7 +98,7 @@ export default class TrackVADEmitter extends EventEmitter {
97 98
     /**
98 99
      * Sets up the audio graph in the AudioContext.
99 100
      *
100
-     * @returns {Promise<void>}
101
+     * @returns {void}
101 102
      */
102 103
     _initializeAudioContext() {
103 104
         this._audioSource = this._audioContext.createMediaStreamSource(this._localTrack.stream);
@@ -132,11 +133,14 @@ export default class TrackVADEmitter extends EventEmitter {
132 133
 
133 134
         for (; i + this._vadSampleSize < completeInData.length; i += this._vadSampleSize) {
134 135
             const pcmSample = completeInData.slice(i, i + this._vadSampleSize);
135
-            const vadScore = this._vadProcessor.calculateAudioFrameVAD(pcmSample);
136
+
137
+            // The VAD processor might change the values inside the array so we make a copy.
138
+            const vadScore = this._vadProcessor.calculateAudioFrameVAD(pcmSample.slice());
136 139
 
137 140
             this.emit(VAD_SCORE_PUBLISHED, {
138 141
                 timestamp: sampleTimestamp,
139 142
                 score: vadScore,
143
+                pcmData: pcmSample,
140 144
                 deviceId: this._localTrack.getDeviceId()
141 145
             });
142 146
         }

+ 219
- 0
modules/detection/VADAudioAnalyser.js Ver arquivo

@@ -0,0 +1,219 @@
1
+import { EventEmitter } from 'events';
2
+import { getLogger } from 'jitsi-meet-logger';
3
+
4
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
5
+
6
+import { VAD_SCORE_PUBLISHED, DETECTOR_STATE_CHANGE } from './DetectionEvents';
7
+import TrackVADEmitter from './TrackVADEmitter';
8
+
9
+const logger = getLogger(__filename);
10
+
11
+/**
12
+ * Sample rate of TrackVADEmitter, it defines how many audio samples are processed at a time.
13
+ * @type {number}
14
+ */
15
+const VAD_EMITTER_SAMPLE_RATE = 4096;
16
+
17
+/**
18
+ * Connects a TrackVADEmitter to the target conference local audio track and manages various services that use
19
+ * the data to produce audio analytics (VADTalkMutedDetection and VADNoiseDetection).
20
+ */
21
+export default class VADAudioAnalyser extends EventEmitter {
22
+    /**
23
+     * Creates <tt>VADAudioAnalyser</tt>
24
+     * @param {JitsiConference} conference - JitsiConference instance that created us.
25
+     * @param {Object} createVADProcessor - Function that creates a Voice activity detection processor. The processor
26
+     * needs to implement the following functions:
27
+     * - <tt>getSampleLength()</tt> - Returns the sample size accepted by getSampleLength.
28
+     * - <tt>getRequiredPCMFrequency()</tt> - Returns the PCM frequency at which the processor operates.
29
+     * - <tt>calculateAudioFrameVAD(pcmSample)</tt> - Process a 32 float pcm sample of getSampleLength size.
30
+     * @constructor
31
+     */
32
+    constructor(conference, createVADProcessor) {
33
+        super();
34
+
35
+        /**
36
+         * Member function that instantiates a VAD processor.
37
+         */
38
+        this._createVADProcessor = createVADProcessor;
39
+
40
+        /**
41
+         * Current {@link TrackVADEmitter}. VAD Emitter uses a {@link JitsiLocalTrack} and VAD processor to generate
42
+         * period voice probability scores.
43
+         */
44
+        this._vadEmitter = null;
45
+
46
+        /**
47
+         * Current state of the _vadEmitter
48
+         */
49
+        this._isVADEmitterRunning = false;
50
+
51
+        /**
52
+         * Array of currently attached VAD processing services.
53
+         */
54
+        this._detectionServices = [];
55
+
56
+        /**
57
+         * Promise used to chain create and destroy operations associated with TRACK_ADDED and TRACK_REMOVED events
58
+         * coming from the conference.
59
+         * Because we have an async created component (VAD Processor) we need to make sure that it's initialized before
60
+         * we destroy it ( when changing the device for instance), or when we use it from an external point of entry
61
+         * i.e. (TRACK_MUTE_CHANGED event callback).
62
+         */
63
+        this._vadInitTracker = Promise.resolve();
64
+
65
+        /**
66
+         * Listens for {@link TrackVADEmitter} events and processes them.
67
+         */
68
+        this._processVADScore = this._processVADScore.bind(this);
69
+
70
+        conference.on(JitsiConferenceEvents.TRACK_ADDED, this._trackAdded.bind(this));
71
+        conference.on(JitsiConferenceEvents.TRACK_REMOVED, this._trackRemoved.bind(this));
72
+        conference.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, this._trackMuteChanged.bind(this));
73
+    }
74
+
75
+    /**
76
+     * Attach a VAD detector service to the analyser and handle it's state changes.
77
+     *
78
+     * @param {Object} vadTMDetector
79
+     */
80
+    addVADDetectionService(vadService) {
81
+        this._detectionServices.push(vadService);
82
+        vadService.on(DETECTOR_STATE_CHANGE, () => {
83
+            // When the state of a detector changes check if there are any active detectors attached so that
84
+            // the _vadEmitter doesn't run needlessly.
85
+            const activeDetector = this._detectionServices.filter(detector => detector.isActive() === true);
86
+
87
+            // If there are no active detectors running and the vadEmitter is running then stop the emitter as it is
88
+            // uses a considerable amount of CPU. Otherwise start the service if it's stopped and there is a detector
89
+            // that needs it.
90
+            if (!activeDetector.length && this._isVADEmitterRunning) {
91
+                this._stopVADEmitter();
92
+            } else if (!this._isVADEmitterRunning) {
93
+                this._startVADEmitter();
94
+            }
95
+        });
96
+    }
97
+
98
+    /**
99
+     * Start the {@link TrackVADEmitter} and attach the event listener.
100
+     * @returns {void}
101
+     */
102
+    _startVADEmitter() {
103
+        this._vadEmitter.on(VAD_SCORE_PUBLISHED, this._processVADScore);
104
+        this._vadEmitter.start();
105
+        this._isVADEmitterRunning = true;
106
+    }
107
+
108
+    /**
109
+     * Stop the {@link TrackVADEmitter} and detach the event listener.
110
+     * @returns {void}
111
+     */
112
+    _stopVADEmitter() {
113
+        this._vadEmitter.removeListener(VAD_SCORE_PUBLISHED, this._processVADScore);
114
+        this._vadEmitter.stop();
115
+        this._isVADEmitterRunning = false;
116
+    }
117
+
118
+    /**
119
+     * Listens for {@link TrackVADEmitter} events and directs them to attached services as needed.
120
+     *
121
+     * @param {Object} vadScore -VAD score emitted by {@link TrackVADEmitter}
122
+     * @param {Date}   vadScore.timestamp - Exact time at which processed PCM sample was generated.
123
+     * @param {number} vadScore.score - VAD score on a scale from 0 to 1 (i.e. 0.7)
124
+     * @param {Float32Array} pcmData - Raw PCM data with which the VAD score was calculated.
125
+     * @param {string} vadScore.deviceId - Device id of the associated track.
126
+     * @listens VAD_SCORE_PUBLISHED
127
+     */
128
+    _processVADScore(vadScore) {
129
+        for (const detector of this._detectionServices) {
130
+            detector.processVADScore(vadScore);
131
+        }
132
+    }
133
+
134
+    /**
135
+     * Change the isMuted state of all attached detection services.
136
+     *
137
+     * @param {boolean} isMuted
138
+     */
139
+    _changeDetectorsMuteState(isMuted) {
140
+        for (const detector of this._detectionServices) {
141
+            detector.changeMuteState(isMuted);
142
+        }
143
+    }
144
+
145
+    /**
146
+     * Notifies the detector that a track was added to the associated {@link JitsiConference}.
147
+     * Only take into account local audio tracks.
148
+     * @param {JitsiTrack} track - The added track.
149
+     * @returns {void}
150
+     * @listens TRACK_ADDED
151
+     */
152
+    _trackAdded(track) {
153
+        if (track.isLocalAudioTrack()) {
154
+            // Keep a track promise so we take into account successive TRACK_ADD events being generated so that we
155
+            // destroy/create the processing context in the proper order.
156
+            this._vadInitTracker = this._vadInitTracker.then(() => this._createVADProcessor())
157
+                .then(vadProcessor =>
158
+                    TrackVADEmitter.create(track.getDeviceId(), VAD_EMITTER_SAMPLE_RATE, vadProcessor)
159
+                )
160
+                .then(vadEmitter => {
161
+                    logger.debug('Created VAD emitter for track: ', track.getTrackLabel());
162
+
163
+                    this._vadEmitter = vadEmitter;
164
+
165
+                    // Iterate through the detection services and set their appropriate mute state, depending on
166
+                    // service this will trigger a DETECTOR_STATE_CHANGE which in turn might start the _vadEmitter.
167
+                    this._changeDetectorsMuteState(track.isMuted());
168
+                });
169
+        }
170
+    }
171
+
172
+    /**
173
+     * Notifies the detector that the mute state of a {@link JitsiConference} track has changed. Only takes into account
174
+     * local audio tracks.
175
+     * @param {JitsiTrack} track - The track whose mute state has changed.
176
+     * @returns {void}
177
+     * @listens TRACK_MUTE_CHANGED
178
+     */
179
+    _trackMuteChanged(track) {
180
+        if (track.isLocalAudioTrack()) {
181
+            // On a mute toggle reset the state.
182
+            this._vadInitTracker = this._vadInitTracker.then(() => {
183
+                // Set mute status for the attached detection services.
184
+                this._changeDetectorsMuteState(track.isMuted());
185
+            });
186
+        }
187
+    }
188
+
189
+    /**
190
+     * Notifies the detector that a track associated with the {@link JitsiConference} was removed. Only takes into
191
+     * account local audio tracks. Cleans up resources associated with the track and resets the processing context.
192
+     *
193
+     * @param {JitsiTrack} track - The removed track.
194
+     * @returns {void}
195
+     * @listens TRACK_REMOVED
196
+     */
197
+    _trackRemoved(track) {
198
+        if (track.isLocalAudioTrack()) {
199
+            // Use the promise to make sure operations are in sequence.
200
+            this._vadInitTracker = this._vadInitTracker.then(() => {
201
+                logger.debug('Removing track from VAD detection - ', track.getTrackLabel());
202
+
203
+                // Track was removed, clean up and set appropriate states.
204
+                if (this._vadEmitter) {
205
+                    this._stopVADEmitter();
206
+                    this._vadEmitter.destroy();
207
+                    this._vadEmitter = null;
208
+                }
209
+
210
+                // Reset state of detectors when active track is removed.
211
+                for (const detector of this._detectionServices) {
212
+                    detector.reset();
213
+                }
214
+            });
215
+        }
216
+    }
217
+
218
+
219
+}

+ 187
- 0
modules/detection/VADNoiseDetection.js Ver arquivo

@@ -0,0 +1,187 @@
1
+import { EventEmitter } from 'events';
2
+
3
+import { calculateAverage, filterPositiveValues } from '../util/MathUtil';
4
+
5
+import { VAD_NOISY_DEVICE, DETECTOR_STATE_CHANGE } from './DetectionEvents';
6
+
7
+/**
8
+ * The average value VAD needs to be under over a period of time to be considered noise.
9
+ * @type {number}
10
+ */
11
+const VAD_NOISE_AVG_THRESHOLD = 0.2;
12
+
13
+/**
14
+ * The average values that audio input need to be over to be considered loud.
15
+ * @type {number}
16
+ */
17
+const NOISY_AUDIO_LEVEL_THRESHOLD = 0.040;
18
+
19
+/**
20
+ * The value that a VAD score needs to be under in order for processing to begin.
21
+ * @type {number}
22
+ */
23
+const VAD_SCORE_TRIGGER = 0.2;
24
+
25
+/**
26
+ * The value that a VAD score needs to be under in order for processing to begin.
27
+ * @type {number}
28
+ */
29
+const AUDIO_LEVEL_SCORE_TRIGGER = 0.020;
30
+
31
+/**
32
+ * Time span over which we calculate an average score used to determine if we trigger the event.
33
+ * @type {number}
34
+ */
35
+const PROCESS_TIME_FRAME_SPAN_MS = 1500;
36
+
37
+/**
38
+ * Detect if provided VAD score and PCM data is considered noise.
39
+ */
40
+export default class VADNoiseDetection extends EventEmitter {
41
+    /**
42
+     * Creates <tt>VADNoiseDetection</tt>
43
+     *
44
+     * @constructor
45
+     */
46
+    constructor() {
47
+        super();
48
+
49
+        /**
50
+         * Flag which denotes the current state of the detection service i.e.if there is already a processing operation
51
+         * ongoing.
52
+         */
53
+        this._processing = false;
54
+
55
+        /**
56
+         * Buffer that keeps the VAD scores for a period of time.
57
+         */
58
+        this._scoreArray = [];
59
+
60
+        /**
61
+         * Buffer that keeps audio level samples for a period of time.
62
+         */
63
+        this._audioLvlArray = [];
64
+
65
+        /**
66
+         * Current state of the service, if it's not active no processing will occur.
67
+         */
68
+        this._active = false;
69
+
70
+        this._calculateNoisyScore = this._calculateNoisyScore.bind(this);
71
+    }
72
+
73
+    /**
74
+     * Compute cumulative VAD score and PCM audio levels once the PROCESS_TIME_FRAME_SPAN_MS timeout has elapsed.
75
+     * If the score is above the set threshold fire the event.
76
+     * @returns {void}
77
+     * @fires VAD_NOISY_DEVICE
78
+     */
79
+    _calculateNoisyScore() {
80
+        const scoreAvg = calculateAverage(this._scoreArray);
81
+        const audioLevelAvg = calculateAverage(this._audioLvlArray);
82
+
83
+        if (scoreAvg < VAD_NOISE_AVG_THRESHOLD && audioLevelAvg > NOISY_AUDIO_LEVEL_THRESHOLD) {
84
+            this.emit(VAD_NOISY_DEVICE);
85
+
86
+            this._setActiveState(false);
87
+        }
88
+
89
+        // We reset the context in case a new process phase needs to be triggered.
90
+        this.reset();
91
+    }
92
+
93
+    /**
94
+     * Record the vad score and average volume in the appropriate buffers.
95
+     *
96
+     * @param {number} vadScore
97
+     * @param {number} avgAudioLvl - average audio level of the PCM sample associated with the VAD score.s
98
+     */
99
+    _recordValues(vadScore, avgAudioLvl) {
100
+        this._scoreArray.push(vadScore);
101
+        this._audioLvlArray.push(avgAudioLvl);
102
+    }
103
+
104
+    /**
105
+     * Set the active state of the detection service and notify any listeners.
106
+     *
107
+     * @param {boolean} active
108
+     * @fires DETECTOR_STATE_CHANGE
109
+     */
110
+    _setActiveState(active) {
111
+        this._active = active;
112
+        this.emit(DETECTOR_STATE_CHANGE, this._active);
113
+    }
114
+
115
+    /**
116
+     * Change the state according to the muted status of the tracked device.
117
+     *
118
+     * @param {boolean} isMuted - Is the device muted or not.
119
+     */
120
+    changeMuteState(isMuted) {
121
+        // This service only needs to run when the microphone is not muted.
122
+        this._setActiveState(!isMuted);
123
+        this.reset();
124
+    }
125
+
126
+    /**
127
+     * Check whether or not the service is active or not.
128
+     *
129
+     * @returns {boolean}
130
+     */
131
+    isActive() {
132
+        return this._active;
133
+    }
134
+
135
+    /**
136
+     * Reset the processing context, clear buffers, cancel the timeout trigger.
137
+     *
138
+     * @returns {void}
139
+     */
140
+    reset() {
141
+        this._processing = false;
142
+        this._scoreArray = [];
143
+        this._audioLvlArray = [];
144
+        clearTimeout(this._processTimeout);
145
+    }
146
+
147
+    /**
148
+     * Listens for {@link TrackVADEmitter} events and processes them.
149
+     *
150
+     * @param {Object} vadScore -VAD score emitted by {@link TrackVADEmitter}
151
+     * @param {Date}   vadScore.timestamp - Exact time at which processed PCM sample was generated.
152
+     * @param {number} vadScore.score - VAD score on a scale from 0 to 1 (i.e. 0.7)
153
+     * @param {Float32Array} vadScore.pcmData - Raw PCM Data associated with the VAD score.
154
+     * @param {string} vadScore.deviceId - Device id of the associated track.
155
+     * @listens VAD_SCORE_PUBLISHED
156
+     */
157
+    processVADScore(vadScore) {
158
+        if (!this._active) {
159
+            return;
160
+        }
161
+
162
+        // There is a processing phase on going, add score to buffer array.
163
+        if (this._processing) {
164
+            // Filter and calculate sample average so we don't have to process one large array at a time.
165
+            const posAudioLevels = filterPositiveValues(vadScore.pcmData);
166
+
167
+            this._recordValues(vadScore.score, calculateAverage(posAudioLevels));
168
+
169
+            return;
170
+        }
171
+
172
+        // If the VAD score for the sample is low and audio level has a high enough level we can start listening for
173
+        // noise
174
+        if (vadScore.score < VAD_SCORE_TRIGGER) {
175
+            const posAudioLevels = filterPositiveValues(vadScore.pcmData);
176
+            const avgAudioLvl = calculateAverage(posAudioLevels);
177
+
178
+            if (avgAudioLvl > AUDIO_LEVEL_SCORE_TRIGGER) {
179
+                this._processing = true;
180
+                this._recordValues(vadScore.score, avgAudioLvl);
181
+
182
+                // Once the preset timeout executes the final score will be calculated.
183
+                this._processTimeout = setTimeout(this._calculateNoisyScore, PROCESS_TIME_FRAME_SPAN_MS);
184
+            }
185
+        }
186
+    }
187
+}

+ 57
- 158
modules/detection/VADTalkMutedDetection.js Ver arquivo

@@ -1,12 +1,9 @@
1 1
 import { EventEmitter } from 'events';
2
-import { getLogger } from 'jitsi-meet-logger';
3 2
 
4
-import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
3
+import { calculateAverage } from '../util/MathUtil';
5 4
 
6
-import { VAD_SCORE_PUBLISHED, VAD_TALK_WHILE_MUTED } from './DetectionEvents';
7
-import TrackVADEmitter from './TrackVADEmitter';
5
+import { VAD_TALK_WHILE_MUTED, DETECTOR_STATE_CHANGE } from './DetectionEvents';
8 6
 
9
-const logger = getLogger(__filename);
10 7
 
11 8
 /**
12 9
  * The threshold which the average VAD values for a span of time needs to exceed to trigger an event.
@@ -25,42 +22,24 @@ const VAD_VOICE_LEVEL = 0.9;
25 22
  * Sample rate of TrackVADEmitter, it defines how many audio samples are processed at a time.
26 23
  * @type {number}
27 24
  */
28
-const VAD_EMITTER_SAMPLE_RATE = 4096;
29 25
 
30 26
 /**
31 27
  * Time span over which we calculate an average score used to determine if we trigger the event.
32 28
  * @type {number}
33 29
  */
34
-const PROCESS_TIME_FRAME_SPAN_MS = 1500;
30
+const PROCESS_TIME_FRAME_SPAN_MS = 700;
35 31
 
36 32
 /**
37
- * Detect user trying to speak while is locally muted and fires an event using a TrackVADEmitter.
33
+ * Detect if provided VAD score which is generated on a muted device is voice and fires an event.
38 34
  */
39 35
 export default class VADTalkMutedDetection extends EventEmitter {
40 36
     /**
41 37
      * Creates <tt>VADTalkMutedDetection</tt>
42
-     * @param {JitsiConference} conference - JitsiConference instance that created us.
43
-     * @param {Object} createVADProcessor - Function that creates a Voice activity detection processor. The processor
44
-     * needs to implement the following functions:
45
-     * - <tt>getSampleLength()</tt> - Returns the sample size accepted by getSampleLength.
46
-     * - <tt>getRequiredPCMFrequency()</tt> - Returns the PCM frequency at which the processor operates.
47
-     * - <tt>calculateAudioFrameVAD(pcmSample)</tt> - Process a 32 float pcm sample of getSampleLength size.
48 38
      * @constructor
49 39
      */
50
-    constructor(conference, createVADProcessor) {
40
+    constructor() {
51 41
         super();
52 42
 
53
-        /**
54
-         * Member function that instantiates a VAD processor.
55
-         */
56
-        this._createVADProcessor = createVADProcessor;
57
-
58
-        /**
59
-         * Current {@link TrackVADEmitter}. VAD Emitter uses a {@link JitsiLocalTrack} and VAD processor to generate
60
-         * period voice probability scores.
61
-         */
62
-        this._vadEmitter = null;
63
-
64 43
         /**
65 44
          * Flag which denotes the current state of the detection service i.e.if there is already a processing operation
66 45
          * ongoing.
@@ -73,73 +52,62 @@ export default class VADTalkMutedDetection extends EventEmitter {
73 52
         this._scoreArray = [];
74 53
 
75 54
         /**
76
-         * Promise used to chain create and destroy operations associated with TRACK_ADDED and TRACK_REMOVED events
77
-         * coming from the conference.
78
-         * Because we have an async created component (VAD Processor) we need to make sure that it's initialized before
79
-         * we destroy it ( when changing the device for instance), or when we use it from an external point of entry
80
-         * i.e. (TRACK_MUTE_CHANGED event callback).
55
+         * Current mute state of the audio track being monitored.
81 56
          */
82
-        this._vadInitTracker = Promise.resolve();
57
+        this._active = false;
83 58
 
84
-        /**
85
-         * Listens for {@link TrackVADEmitter} events and processes them.
86
-         */
87
-        this._processVADScore = this._processVADScore.bind(this);
88
-
89
-        /**
90
-         * {@link JitsiConference} bindings.
91
-         */
92
-        conference.on(JitsiConferenceEvents.TRACK_ADDED, this._trackAdded.bind(this));
93
-        conference.on(JitsiConferenceEvents.TRACK_REMOVED, this._trackRemoved.bind(this));
94
-        conference.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, this._trackMuteChanged.bind(this));
59
+        this._calculateVADScore = this._calculateVADScore.bind(this);
95 60
     }
96 61
 
97 62
     /**
98
-     * Start the {@link TrackVADEmitter} and attach the event listener.
63
+     * Compute cumulative VAD score function called once the PROCESS_TIME_FRAME_SPAN_MS timeout has elapsed.
99 64
      * @returns {void}
65
+     * @fires VAD_TALK_WHILE_MUTED
100 66
      */
101
-    _startVADEmitter() {
102
-        this._vadEmitter.on(VAD_SCORE_PUBLISHED, this._processVADScore);
103
-        this._vadEmitter.start();
67
+    _calculateVADScore() {
68
+        const score = calculateAverage(this._scoreArray);
69
+
70
+        if (score > VAD_AVG_THRESHOLD) {
71
+            this.emit(VAD_TALK_WHILE_MUTED);
72
+
73
+            // Event was fired. Stop event emitter and remove listeners so no residue events kick off after this point
74
+            // and a single VAD_TALK_WHILE_MUTED is generated per mic muted state.
75
+            this._setActiveState(false);
76
+        }
77
+
78
+        // We reset the context in case a new process phase needs to be triggered.
79
+        this.reset();
104 80
     }
105 81
 
106 82
     /**
107
-     * Stop the {@link TrackVADEmitter} and detach the event listener.
108
-     * @returns {void}
83
+     * Set the active state of the detection service and notify any listeners.
84
+     *
85
+     * @param {boolean} active
86
+     * @fires DETECTOR_STATE_CHANGE
109 87
      */
110
-    _stopVADEmitter() {
111
-        this._vadEmitter.removeListener(VAD_SCORE_PUBLISHED, this._processVADScore);
112
-        this._vadEmitter.stop();
88
+    _setActiveState(active) {
89
+        this._active = active;
90
+        this.emit(DETECTOR_STATE_CHANGE, this._active);
113 91
     }
114 92
 
115 93
     /**
116
-     * Calculates the average value of a Float32Array.
94
+     * Change the state according to the muted status of the tracked device.
117 95
      *
118
-     * @param {Float32Array} scoreArray - Array of vad scores.
119
-     * @returns {number} - Score average.
96
+     * @param {boolean} isMuted - Is the device muted or not.
120 97
      */
121
-    _calculateAverage(scoreArray) {
122
-        return scoreArray.length > 0 ? scoreArray.reduce((a, b) => a + b) / scoreArray.length : 0;
98
+    changeMuteState(isMuted) {
99
+        // This service only needs to run when the microphone is muted.
100
+        this._setActiveState(isMuted);
101
+        this.reset();
123 102
     }
124 103
 
125 104
     /**
126
-     * Compute cumulative VAD score function called once the PROCESS_TIME_FRAME_SPAN_MS timeout has elapsed.
127
-     * @returns {void}
128
-     * @fires VAD_TALK_WHILE_MUTED
105
+     * Check whether or not the service is active or not.
106
+     *
107
+     * @returns {boolean}
129 108
      */
130
-    _calculateVADScore() {
131
-        const score = this._calculateAverage(this._scoreArray);
132
-
133
-        if (score > VAD_AVG_THRESHOLD) {
134
-            this.emit(VAD_TALK_WHILE_MUTED, {});
135
-
136
-            // Event was fired. Stop event emitter and remove listeners so no residue events kick off after this point
137
-            // and a single VAD_TALK_WHILE_MUTED is generated per mic muted state.
138
-            this._stopVADEmitter();
139
-        }
140
-
141
-        // We reset the context in case a new process phase needs to be triggered.
142
-        this._reset();
109
+    isActive() {
110
+        return this._active;
143 111
     }
144 112
 
145 113
     /**
@@ -151,106 +119,37 @@ export default class VADTalkMutedDetection extends EventEmitter {
151 119
      * @param {string} vadScore.deviceId - Device id of the associated track.
152 120
      * @listens VAD_SCORE_PUBLISHED
153 121
      */
154
-    _processVADScore(vadScore) {
155
-        // Because we remove all listeners on the vadEmitter once the main event is triggered,
156
-        // there is no need to check for rogue events.
157
-        if (vadScore.score > VAD_VOICE_LEVEL && !this._processing) {
158
-            this._processing = true;
159
-
160
-            // Start gathering VAD scores for the configured period of time.
161
-            this._processTimeout = setTimeout(this._calculateVADScore.bind(this), PROCESS_TIME_FRAME_SPAN_MS);
122
+    processVADScore(vadScore) {
123
+        if (!this._active) {
124
+            return;
162 125
         }
163 126
 
164 127
         // There is a processing phase on going, add score to buffer array.
165 128
         if (this._processing) {
166 129
             this._scoreArray.push(vadScore.score);
167
-        }
168
-    }
169 130
 
170
-    /**
171
-     * Reset the processing context, clear buffer, cancel the timeout trigger.
172
-     *
173
-     * @returns {void}
174
-     */
175
-    _reset() {
176
-        this._processing = false;
177
-        this._scoreArray = [];
178
-        clearTimeout(this._processTimeout);
179
-    }
180
-
181
-    /**
182
-     * Notifies the detector that a track was added to the associated {@link JitsiConference}.
183
-     * Only take into account local audio tracks.
184
-     * @param {JitsiTrack} track - The added track.
185
-     * @returns {void}
186
-     * @listens TRACK_ADDED
187
-     */
188
-    _trackAdded(track) {
189
-        if (track.isLocalAudioTrack()) {
190
-            // Keep a track promise so we take into account successive TRACK_ADD events being generated so that we
191
-            // destroy/create the processing context in the proper order.
192
-            this._vadInitTracker = this._vadInitTracker.then(() => this._createVADProcessor())
193
-                .then(vadProcessor =>
194
-                    TrackVADEmitter.create(track.getDeviceId(), VAD_EMITTER_SAMPLE_RATE, vadProcessor)
195
-                )
196
-                .then(vadEmitter => {
197
-                    logger.debug('Created VAD emitter for track: ', track.getTrackLabel());
198
-
199
-                    this._vadEmitter = vadEmitter;
200
-
201
-                    if (track.isMuted()) {
202
-                        this._startVADEmitter();
203
-                    }
204
-                });
131
+            return;
205 132
         }
206
-    }
207 133
 
208
-    /**
209
-     * Notifies the detector that the mute state of a {@link JitsiConference} track has changed. Only takes into account
210
-     * local audio tracks. In case the track was muted the detector starts the {@link TrackVADEmitter} otherwise it's
211
-     * stopped.
212
-     * @param {JitsiTrack} track - The track whose mute state has changed.
213
-     * @returns {void}
214
-     * @listens TRACK_MUTE_CHANGED
215
-     */
216
-    _trackMuteChanged(track) {
217
-        if (track.isLocalAudioTrack()) {
218
-            // On a mute toggle reset the state.
219
-            this._vadInitTracker = this._vadInitTracker.then(() => {
134
+        // Because we remove all listeners on the vadEmitter once the main event is triggered,
135
+        // there is no need to check for rogue events.
136
+        if (vadScore.score > VAD_VOICE_LEVEL) {
137
+            this._processing = true;
138
+            this._scoreArray.push(vadScore.score);
220 139
 
221
-                // Reset the processing context in between muted states so that each individual mute phase can generate
222
-                // it's own event.
223
-                this._reset();
224
-                if (track.isMuted()) {
225
-                    this._startVADEmitter();
226
-                } else {
227
-                    this._stopVADEmitter();
228
-                }
229
-            });
140
+            // Start gathering VAD scores for the configured period of time.
141
+            this._processTimeout = setTimeout(this._calculateVADScore, PROCESS_TIME_FRAME_SPAN_MS);
230 142
         }
231 143
     }
232 144
 
233 145
     /**
234
-     * Notifies the detector that a track associated with the {@link JitsiConference} was removed. Only takes into
235
-     * account local audio tracks. Cleans up resources associated with the track and resets the processing context.
146
+     * Reset the processing context, clear buffer, cancel the timeout trigger.
236 147
      *
237
-     * @param {JitsiTrack} track - The removed track.
238 148
      * @returns {void}
239
-     * @listens TRACK_REMOVED
240 149
      */
241
-    _trackRemoved(track) {
242
-        if (track.isLocalAudioTrack()) {
243
-            // Use the promise to make sure operations are in sequence.
244
-            this._vadInitTracker = this._vadInitTracker.then(() => {
245
-                logger.debug('Removing track from VAD detection - ', track.getTrackLabel());
246
-
247
-                if (this._vadEmitter) {
248
-                    this._stopVADEmitter();
249
-                    this._reset();
250
-                    this._vadEmitter.destroy();
251
-                    this._vadEmitter = null;
252
-                }
253
-            });
254
-        }
150
+    reset() {
151
+        this._processing = false;
152
+        this._scoreArray = [];
153
+        clearTimeout(this._processTimeout);
255 154
     }
256 155
 }

+ 14
- 0
modules/detection/webaudio/WebAudioUtils.js Ver arquivo

@@ -0,0 +1,14 @@
1
+/**
2
+ * Adapter that creates AudioContext objects depending on the browser.
3
+ *
4
+ * @returns {AudioContext} - Return a new AudioContext or undefined if the browser does not support it.
5
+ */
6
+export function createAudioContext(options) {
7
+    const AudioContextImpl = window.AudioContext || window.webkitAudioContext;
8
+
9
+    if (!AudioContextImpl) {
10
+        return undefined;
11
+    }
12
+
13
+    return new AudioContextImpl(options);
14
+}

+ 21
- 0
modules/util/MathUtil.js Ver arquivo

@@ -17,3 +17,24 @@ export function safeCounterIncrement(number) {
17 17
 
18 18
     return nextValue + 1;
19 19
 }
20
+
21
+/**
22
+ * Calculates the average value of am Array of numbers.
23
+ *
24
+ * @param {Float32Array} valueArray - Array of numbers.
25
+ * @returns {number} - Number array average.
26
+ */
27
+export function calculateAverage(valueArray) {
28
+    return valueArray.length > 0 ? valueArray.reduce((a, b) => a + b) / valueArray.length : 0;
29
+}
30
+
31
+
32
+/**
33
+ * Returns only the positive values from an array of numbers.
34
+ *
35
+ * @param {Float32Array} valueArray - Array of vad scores.
36
+ * @returns {Array} - Array of positive numbers.
37
+ */
38
+export function filterPositiveValues(valueArray) {
39
+    return valueArray.filter(value => value >= 0);
40
+}

Carregando…
Cancelar
Salvar