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.

VADReportingService.js 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. // @flow
  2. import EventEmitter from 'events';
  3. import logger from './logger';
  4. import TrackVADEmitter from './TrackVADEmitter';
  5. import { VAD_SCORE_PUBLISHED, VAD_REPORT_PUBLISHED } from './VADEvents';
  6. import type { VADScore } from './TrackVADEmitter';
  7. export type { VADScore };
  8. /**
  9. * Sample rate used by TrackVADEmitter, this value determines how often the ScriptProcessorNode is going to call the
  10. * process audio function and with what sample size.
  11. * Basically lower values mean more callbacks with lower processing times bigger values less callbacks with longer
  12. * processing times. This value is somewhere in the middle, so we strike a balance between flooding with callbacks
  13. * and processing time. Possible values 256, 512, 1024, 2048, 4096, 8192, 16384. Passing other values will default
  14. * to closes neighbor.
  15. */
  16. const SCRIPT_NODE_SAMPLE_RATE = 4096;
  17. /**
  18. * Context that contains the emitter and additional information about the device.
  19. */
  20. type VADDeviceContext = {
  21. /**
  22. * MediaDeviceInfo for associated context
  23. */
  24. deviceInfo: MediaDeviceInfo,
  25. /**
  26. * Array with VAD scores publish from the emitter.
  27. */
  28. scoreArray: Array<VADScore>,
  29. /**
  30. * TrackVADEmitter associated with media device
  31. */
  32. vadEmitter: TrackVADEmitter
  33. };
  34. /**
  35. * The structure used by VADReportingService to relay a score report
  36. */
  37. export type VADReportScore = {
  38. /**
  39. * Device ID associated with the VAD score
  40. */
  41. deviceId: string,
  42. /**
  43. * The PCM score from 0 - 1 i.e. 0.60
  44. */
  45. score: number,
  46. /**
  47. * Epoch time at which PCM was recorded
  48. */
  49. timestamp: number
  50. };
  51. /**
  52. * Voice activity detection reporting service. The service create TrackVADEmitters for the provided devices and
  53. * publishes an average of their VAD score over the specified interval via EventEmitter.
  54. * The service is not reusable if destroyed a new one needs to be created, i.e. when a new device is added to the system
  55. * a new service needs to be created and the old discarded.
  56. */
  57. export default class VADReportingService extends EventEmitter {
  58. /**
  59. * Map containing context for devices currently being monitored by the reporting service.
  60. */
  61. _contextMap: Map<string, VADDeviceContext>;
  62. /**
  63. * State flag, check if the instance was destroyed.
  64. */
  65. _destroyed: boolean = false;
  66. /**
  67. * Delay at which to publish VAD score for monitored devices.
  68. */
  69. _intervalDelay: number;
  70. /**
  71. * Identifier for the interval publishing stats on the set interval.
  72. */
  73. _intervalId: ?IntervalID;
  74. /**
  75. * Constructor.
  76. *
  77. * @param {number} intervalDelay - Delay at which to publish VAD score for monitored devices.
  78. * @param {Function} publishScoreCallBack - Function called on the specific interval with the calculated VAD score.
  79. */
  80. constructor(intervalDelay: number) {
  81. super();
  82. this._contextMap = new Map();
  83. this._intervalDelay = intervalDelay;
  84. logger.log(`Constructed VADReportingService with publish interval of: ${intervalDelay}`);
  85. }
  86. /**
  87. * Factory methods that creates the TrackVADEmitters for the associated array of devices and instantiates
  88. * a VADReportingService.
  89. *
  90. * @param {Array<MediaDeviceInfo>} micDeviceList - Device list that is monitored inside the service.
  91. * @param {number} intervalDelay - Delay at which to publish VAD score for monitored devices.
  92. * @param {Function} publishScoreCallBack - Function called on the specific interval with the calculated VAD score.
  93. *
  94. * @returns {Promise<VADReportingService>}
  95. */
  96. static create(micDeviceList: Array<MediaDeviceInfo>, intervalDelay: number) {
  97. const vadReportingService = new VADReportingService(intervalDelay);
  98. const emitterPromiseArray = [];
  99. // Create a TrackVADEmitter for each provided audioinput device.
  100. for (const micDevice of micDeviceList) {
  101. if (micDevice.kind !== 'audioinput') {
  102. logger.warn(`Provided device ${micDevice.label} -> ${micDevice.deviceId}, is not audioinput ignoring!`);
  103. return;
  104. }
  105. logger.log(`Initializing VAD context for mic: ${micDevice.label} -> ${micDevice.deviceId}`);
  106. const emitterPromise = TrackVADEmitter.create(micDevice.deviceId, SCRIPT_NODE_SAMPLE_RATE).then(emitter => {
  107. emitter.on(VAD_SCORE_PUBLISHED, vadReportingService._devicePublishVADScore.bind(vadReportingService));
  108. return {
  109. vadEmitter: emitter,
  110. deviceInfo: micDevice,
  111. scoreArray: []
  112. };
  113. });
  114. emitterPromiseArray.push(emitterPromise);
  115. }
  116. // Once all the TrackVADEmitter promises are resolved check if all of them resolved properly if not reject
  117. // the promise and clear the already created emitters.
  118. // $FlowFixMe - allSettled is not part of flow prototype even though it's a valid Promise function
  119. return Promise.allSettled(emitterPromiseArray).then(outcomeArray => {
  120. const vadContextArray = [];
  121. const rejectedEmitterPromiseArray = [];
  122. for (const outcome of outcomeArray) {
  123. if (outcome.status === 'fulfilled') {
  124. vadContextArray.push(outcome.value);
  125. } else {
  126. // Promise was rejected.
  127. logger.error(`Create TrackVADEmitter promise failed with ${outcome.reason}`);
  128. rejectedEmitterPromiseArray.push(outcome);
  129. }
  130. }
  131. // Check if there were any rejected promises and clear the already created ones list.
  132. if (rejectedEmitterPromiseArray.length > 0) {
  133. logger.error('Cleaning up remaining VADDeviceContext, due to create fail!');
  134. for (const context of vadContextArray) {
  135. context.vadEmitter.destroy();
  136. }
  137. // Reject create promise if one emitter failed to instantiate, we might one just ignore it,
  138. // leaving it like this for now
  139. throw new Error('Create VADReportingService failed due to TrackVADEmitter creation issues!');
  140. }
  141. vadReportingService._setVADContextArray(vadContextArray);
  142. vadReportingService._startPublish();
  143. return vadReportingService;
  144. });
  145. }
  146. /**
  147. * Destroy TrackVADEmitters and clear the context map.
  148. *
  149. * @returns {void}
  150. */
  151. _clearContextMap() {
  152. for (const vadContext of this._contextMap.values()) {
  153. vadContext.vadEmitter.destroy();
  154. }
  155. this._contextMap.clear();
  156. }
  157. /**
  158. * Set the watched device contexts.
  159. *
  160. * @param {Array<VADDeviceContext>} vadContextArray - List of mics.
  161. * @returns {void}
  162. */
  163. _setVADContextArray(vadContextArray: Array<VADDeviceContext>): void {
  164. for (const vadContext of vadContextArray) {
  165. this._contextMap.set(vadContext.deviceInfo.deviceId, vadContext);
  166. }
  167. }
  168. /**
  169. * Start the setInterval reporting process.
  170. *
  171. * @returns {void}.
  172. */
  173. _startPublish() {
  174. logger.log('VADReportingService started publishing.');
  175. this._intervalId = setInterval(() => {
  176. this._reportVadScore();
  177. }, this._intervalDelay);
  178. }
  179. /**
  180. * Function called at set interval with selected compute. The result will be published on the set callback.
  181. *
  182. * @returns {void}
  183. */
  184. _reportVadScore() {
  185. const vadComputeScoreArray = [];
  186. const computeTimestamp = Date.now();
  187. // Go through each device and compute cumulated VAD score.
  188. for (const [ deviceId, vadContext ] of this._contextMap) {
  189. const nrOfVADScores = vadContext.scoreArray.length;
  190. let vadSum = 0;
  191. vadContext.scoreArray.forEach(vadScore => {
  192. vadSum += vadScore.score;
  193. });
  194. // TODO For now we just calculate the average score for each device, more compute algorithms will be added.
  195. const avgVAD = vadSum / nrOfVADScores;
  196. vadContext.scoreArray = [];
  197. vadComputeScoreArray.push({
  198. timestamp: computeTimestamp,
  199. score: avgVAD,
  200. deviceId
  201. });
  202. }
  203. this.emit(VAD_REPORT_PUBLISHED, vadComputeScoreArray);
  204. }
  205. /**
  206. * Callback method passed to vad emitters in order to publish their score.
  207. *
  208. * @param {VADScore} vadScore - Mic publishing the score.
  209. * @returns {void}
  210. */
  211. _devicePublishVADScore(vadScore: VADScore) {
  212. const context = this._contextMap.get(vadScore.deviceId);
  213. if (context) {
  214. context.scoreArray.push(vadScore);
  215. }
  216. }
  217. /**
  218. * Destroy the VADReportingService, stops the setInterval reporting, destroys the emitters and clears the map.
  219. * After this call the instance is no longer usable.
  220. *
  221. * @returns {void}.
  222. */
  223. destroy() {
  224. if (this._destroyed) {
  225. return;
  226. }
  227. logger.log('Destroying VADReportingService.');
  228. if (this._intervalId) {
  229. clearInterval(this._intervalId);
  230. this._intervalId = null;
  231. }
  232. this._clearContextMap();
  233. this._destroyed = true;
  234. }
  235. }