You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VADNoiseDetection.js 5.7KB

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