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.

NoiseSuppressionEffect.ts 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { INoiseSuppressionConfig } from '../../base/config/configType';
  2. import { getBaseUrl } from '../../base/util/helpers';
  3. import logger from './logger';
  4. interface IKrispState {
  5. filterNode?: AudioWorkletNode;
  6. filterNodeReady: boolean;
  7. sdk: any;
  8. sdkInitialized: boolean;
  9. }
  10. const krispState: IKrispState = {
  11. filterNode: undefined,
  12. filterNodeReady: false,
  13. sdk: undefined,
  14. sdkInitialized: false
  15. };
  16. let audioContext: AudioContext;
  17. /**
  18. * Class Implementing the effect interface expected by a JitsiLocalTrack.
  19. * Effect applies rnnoise denoising on a audio JitsiLocalTrack.
  20. */
  21. export class NoiseSuppressionEffect {
  22. /**
  23. * Source that will be attached to the track affected by the effect.
  24. */
  25. private _audioSource: MediaStreamAudioSourceNode;
  26. /**
  27. * Destination that will contain denoised audio from the audio worklet.
  28. */
  29. private _audioDestination: MediaStreamAudioDestinationNode;
  30. /**
  31. * `AudioWorkletProcessor` associated node.
  32. */
  33. private _noiseSuppressorNode?: AudioWorkletNode;
  34. /**
  35. * Audio track extracted from the original MediaStream to which the effect is applied.
  36. */
  37. private _originalMediaTrack: MediaStreamTrack;
  38. /**
  39. * Noise suppressed audio track extracted from the media destination node.
  40. */
  41. private _outputMediaTrack: MediaStreamTrack;
  42. /**
  43. * Configured options for noise suppression.
  44. */
  45. private _options?: INoiseSuppressionConfig;
  46. /**
  47. * Instantiates a noise suppressor audio effect which will use either rnnoise or krisp.
  48. *
  49. * @param {INoiseSuppressionConfig} options - Configured options.
  50. */
  51. constructor(options?: INoiseSuppressionConfig) {
  52. this._options = options;
  53. const useKrisp = options?.krisp?.enabled;
  54. logger.info(`NoiseSuppressionEffect created with ${useKrisp ? 'Krisp' : 'RNNoise'}`);
  55. }
  56. /**
  57. * Effect interface called by source JitsiLocalTrack.
  58. * Applies effect that uses a {@code NoiseSuppressor} service initialized with {@code RnnoiseProcessor}
  59. * for denoising.
  60. *
  61. * @param {MediaStream} audioStream - Audio stream which will be mixed with _mixAudio.
  62. * @returns {MediaStream} - MediaStream containing both audio tracks mixed together.
  63. */
  64. startEffect(audioStream: MediaStream): MediaStream {
  65. this._originalMediaTrack = audioStream.getAudioTracks()[0];
  66. if (!audioContext) {
  67. audioContext = new AudioContext();
  68. }
  69. this._audioSource = audioContext.createMediaStreamSource(audioStream);
  70. this._audioDestination = audioContext.createMediaStreamDestination();
  71. this._outputMediaTrack = this._audioDestination.stream.getAudioTracks()[0];
  72. let init;
  73. if (this._options?.krisp?.enabled) {
  74. init = _initializeKrisp(this._options).then(filterNode => {
  75. this._noiseSuppressorNode = filterNode;
  76. if (krispState.filterNodeReady) {
  77. // @ts-ignore
  78. krispState.filterNode?.enable();
  79. }
  80. });
  81. } else {
  82. init = _initializeKRnnoise().then(filterNode => {
  83. this._noiseSuppressorNode = filterNode;
  84. });
  85. }
  86. // Connect the audio processing graph MediaStream -> AudioWorkletNode -> MediaStreamAudioDestinationNode
  87. init.then(() => {
  88. if (this._noiseSuppressorNode) {
  89. this._audioSource.connect(this._noiseSuppressorNode);
  90. this._noiseSuppressorNode.connect(this._audioDestination);
  91. }
  92. });
  93. // Sync the effect track muted state with the original track state.
  94. this._outputMediaTrack.enabled = this._originalMediaTrack.enabled;
  95. // We enable the audio on the original track because mute/unmute action will only affect the audio destination
  96. // output track from this point on.
  97. this._originalMediaTrack.enabled = true;
  98. return this._audioDestination.stream;
  99. }
  100. /**
  101. * Checks if the JitsiLocalTrack supports this effect.
  102. *
  103. * @param {JitsiLocalTrack} sourceLocalTrack - Track to which the effect will be applied.
  104. * @returns {boolean} - Returns true if this effect can run on the specified track, false otherwise.
  105. */
  106. isEnabled(sourceLocalTrack: any): boolean {
  107. // JitsiLocalTracks needs to be an audio track.
  108. return sourceLocalTrack.isAudioTrack();
  109. }
  110. /**
  111. * Clean up resources acquired by noise suppressor and rnnoise processor.
  112. *
  113. * @returns {void}
  114. */
  115. stopEffect(): void {
  116. // Sync original track muted state with effect state before removing the effect.
  117. this._originalMediaTrack.enabled = this._outputMediaTrack.enabled;
  118. if (this._options?.krisp?.enabled) {
  119. // When using Krisp we'll just disable the filter which we'll keep reusing.
  120. // @ts-ignore
  121. this._noiseSuppressorNode?.disable();
  122. } else {
  123. // Technically after this process the Audio Worklet along with it's resources should be garbage collected,
  124. // however on chrome there seems to be a problem as described here:
  125. // https://bugs.chromium.org/p/chromium/issues/detail?id=1298955
  126. this._noiseSuppressorNode?.port?.close();
  127. }
  128. this._audioDestination?.disconnect();
  129. this._noiseSuppressorNode?.disconnect();
  130. this._audioSource?.disconnect();
  131. audioContext.suspend();
  132. }
  133. }
  134. /**
  135. * Initializes the Krisp SDK and creates the filter node.
  136. *
  137. * @param {INoiseSuppressionConfig} options - Krisp options.
  138. *
  139. * @returns {Promise<AudioWorkletNode | undefined>}
  140. */
  141. async function _initializeKrisp(options: INoiseSuppressionConfig): Promise<AudioWorkletNode | undefined> {
  142. await audioContext.resume();
  143. if (!krispState.sdk) {
  144. const baseUrl = `${getBaseUrl()}libs/krisp`;
  145. const { default: KrispSDK } = await import(/* webpackIgnore: true */ `${baseUrl}/krispsdk.mjs`);
  146. krispState.sdk = new KrispSDK({
  147. params: {
  148. models: {
  149. model8: `${baseUrl}/models/model_8.kw`,
  150. model16: `${baseUrl}/models/model_16.kw`,
  151. model32: `${baseUrl}/models/model_32.kw`
  152. },
  153. logProcessStats: options?.krisp?.logProcessStats,
  154. debugLogs: options?.krisp?.debugLogs
  155. },
  156. callbacks: {}
  157. });
  158. }
  159. if (!krispState.sdkInitialized) {
  160. // @ts-ignore
  161. await krispState.sdk?.init();
  162. krispState.sdkInitialized = true;
  163. }
  164. if (!krispState.filterNode) {
  165. try {
  166. // @ts-ignore
  167. krispState.filterNode = await krispState.sdk?.createNoiseFilter(audioContext, () => {
  168. logger.info('Krisp audio filter ready');
  169. // Enable audio filtering.
  170. // @ts-ignore
  171. krispState.filterNode?.enable();
  172. krispState.filterNodeReady = true;
  173. });
  174. } catch (e) {
  175. logger.error('Failed to create Krisp noise filter', e);
  176. krispState.filterNode = undefined;
  177. krispState.filterNodeReady = false;
  178. }
  179. }
  180. return krispState.filterNode;
  181. }
  182. /**
  183. * Initializes the RNNoise audio worklet and creates the filter node.
  184. *
  185. * @returns {Promise<AudioWorkletNode | undefined>}
  186. */
  187. async function _initializeKRnnoise(): Promise<AudioWorkletNode | undefined> {
  188. await audioContext.resume();
  189. const baseUrl = `${getBaseUrl()}libs/`;
  190. const workletUrl = `${baseUrl}noise-suppressor-worklet.min.js`;
  191. try {
  192. await audioContext.audioWorklet.addModule(workletUrl);
  193. } catch (e) {
  194. logger.error('Error while adding audio worklet module: ', e);
  195. return;
  196. }
  197. // After the resolution of module loading, an AudioWorkletNode can be constructed.
  198. return new AudioWorkletNode(audioContext, 'NoiseSuppressorWorklet');
  199. }