Browse Source

feat(noise-suppression): Add noise suppression effect. (#11547)

* add denoise effect

* denoise prototype

* improve rnnoise / add comments

* revert some unnecessary changes

* Add noise suppressor worklet

* Send notification on failure

* address code review

* additional comments

* additional comments

* update package-lock

* fix rebase changes

* update rnnoise npm package

* sort lang

* adjust webpack performance hint

* address code review

* address code review

* switch ns files to typescript

* fix null-loader version, lang sort

* fix lint

* missing import

* fix lint / address code review

* use single action for ns state

* move activation to thunk

* increase node heap

* copy noise-suppressor to deploy

* fix ts lint
factor2
Andrei Gavrilescu 3 years ago
parent
commit
06491e2406
No account linked to committer's email address

+ 3
- 1
.github/workflows/ci.yml View File

@@ -20,4 +20,6 @@ jobs:
20 20
       run: $(exit $(git status --porcelain --untracked-files=no | head -255 | wc -l)) || (echo "Dirty git tree"; git diff; exit 1)
21 21
     - run: npm run lint
22 22
     - run: for file in lang/*.json; do npx --yes jsonlint -q $file || exit 1; done
23
-    - run: make
23
+    - env:
24
+        NODE_OPTIONS: '--max-old-space-size=4096'  
25
+      run: make

+ 3
- 1
Makefile View File

@@ -4,7 +4,7 @@ DEPLOY_DIR = libs
4 4
 LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet
5 5
 OLM_DIR = node_modules/@matrix-org/olm
6 6
 TF_WASM_DIR = node_modules/@tensorflow/tfjs-backend-wasm/dist/
7
-RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist
7
+RNNOISE_WASM_DIR = node_modules/@jitsi/rnnoise-wasm/dist
8 8
 TFLITE_WASM = react/features/stream-effects/virtual-background/vendor/tflite
9 9
 MEET_MODELS_DIR  = react/features/stream-effects/virtual-background/vendor/models
10 10
 FACE_MODELS_DIR = node_modules/@vladmandic/human-models/models
@@ -49,6 +49,8 @@ deploy-appbundle:
49 49
 		$(BUILD_DIR)/analytics-ga.min.js.map \
50 50
 		$(BUILD_DIR)/face-landmarks-worker.min.js \
51 51
 		$(BUILD_DIR)/face-landmarks-worker.min.js.map \
52
+		$(BUILD_DIR)/noise-suppressor-worklet.min.js \
53
+		$(BUILD_DIR)/noise-suppressor-worklet.min.js.map \
52 54
 		$(DEPLOY_DIR)
53 55
 	cp \
54 56
 		$(BUILD_DIR)/close3.min.js \

+ 10
- 1
conference.js View File

@@ -137,6 +137,7 @@ import {
137 137
     submitFeedback
138 138
 } from './react/features/feedback';
139 139
 import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
140
+import { setNoiseSuppressionEnabled } from './react/features/noise-suppression/actions';
140 141
 import {
141 142
     isModerationNotificationDisplayed,
142 143
     showNotification,
@@ -2017,6 +2018,11 @@ export default {
2017 2018
                 }
2018 2019
 
2019 2020
                 if (this._desktopAudioStream) {
2021
+                    // Noise suppression doesn't work with desktop audio because we can't chain
2022
+                    // track effects yet, disable it first.
2023
+                    // We need to to wait for the effect to clear first or it might interfere with the audio mixer.
2024
+                    await APP.store.dispatch(setNoiseSuppressionEnabled(false));
2025
+
2020 2026
                     const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
2021 2027
 
2022 2028
                     // If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing
@@ -2590,9 +2596,12 @@ export default {
2590 2596
 
2591 2597
         APP.UI.addListener(
2592 2598
             UIEvents.AUDIO_DEVICE_CHANGED,
2593
-            micDeviceId => {
2599
+            async micDeviceId => {
2594 2600
                 const audioWasMuted = this.isLocalAudioMuted();
2595 2601
 
2602
+                // Disable noise suppression if it was enabled on the previous track.
2603
+                await APP.store.dispatch(setNoiseSuppressionEnabled(false));
2604
+
2596 2605
                 // When the 'default' mic needs to be selected, we need to
2597 2606
                 // pass the real device id to gUM instead of 'default' in order
2598 2607
                 // to get the correct MediaStreamTrack from chrome because of the

+ 7
- 0
lang/main.json View File

@@ -683,6 +683,10 @@
683 683
         "newDeviceAction": "Use",
684 684
         "newDeviceAudioTitle": "New audio device detected",
685 685
         "newDeviceCameraTitle": "New camera detected",
686
+        "noiseSuppressionDesktopAudioDescription": "Noise suppression can't be enabled while sharing desktop audio, please disable it and try again.",
687
+        "noiseSuppressionFailedTitle": "Failed to start noise suppression",
688
+        "noiseSuppressionNoTrackDescription": "Please unmute your microphone first.",
689
+        "noiseSuppressionStereoDescription": "Stereo audio noise suppression is not currently supported.",
686 690
         "oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
687 691
         "oldElectronClientDescription2": "latest build",
688 692
         "oldElectronClientDescription3": " now!",
@@ -1075,6 +1079,7 @@
1075 1079
             "muteEveryoneElse": "Mute everyone else",
1076 1080
             "muteEveryoneElsesVideoStream": "Stop everyone else's video",
1077 1081
             "muteEveryonesVideoStream": "Stop everyone's video",
1082
+            "noiseSuppression": "Noise suppression",
1078 1083
             "participants": "Participants",
1079 1084
             "pip": "Toggle Picture-in-Picture mode",
1080 1085
             "privateMessage": "Send private message",
@@ -1115,6 +1120,7 @@
1115 1120
         "clap": "Clap",
1116 1121
         "closeChat": "Close chat",
1117 1122
         "closeReactionsMenu": "Close reactions menu",
1123
+        "disableNoiseSuppression": "Disable noise suppression",
1118 1124
         "disableReactionSounds": "You can disable reaction sounds for this meeting",
1119 1125
         "dock": "Dock in main window",
1120 1126
         "documentClose": "Close shared document",
@@ -1151,6 +1157,7 @@
1151 1157
         "noAudioSignalDialInDesc": "You can also dial-in using:",
1152 1158
         "noAudioSignalDialInLinkDesc": "Dial-in numbers",
1153 1159
         "noAudioSignalTitle": "There is no input coming from your mic!",
1160
+        "noiseSuppression": "Noise suppression",
1154 1161
         "noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
1155 1162
         "noisyAudioInputTitle": "Your microphone appears to be noisy!",
1156 1163
         "openChat": "Open chat",

+ 208
- 72
package-lock.json View File

@@ -34,6 +34,7 @@
34 34
         "@hapi/bourne": "2.0.0",
35 35
         "@jitsi/js-utils": "2.0.0",
36 36
         "@jitsi/logger": "2.0.0",
37
+        "@jitsi/rnnoise-wasm": "0.1.0",
37 38
         "@jitsi/rtcstats": "9.2.0",
38 39
         "@material-ui/core": "4.11.3",
39 40
         "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
@@ -53,6 +54,7 @@
53 54
         "@svgr/webpack": "4.3.2",
54 55
         "@tensorflow/tfjs-backend-wasm": "3.13.0",
55 56
         "@tensorflow/tfjs-core": "3.13.0",
57
+        "@types/audioworklet": "0.0.29",
56 58
         "@vladmandic/human": "2.6.5",
57 59
         "@vladmandic/human-models": "2.5.9",
58 60
         "@xmldom/xmldom": "0.7.5",
@@ -78,6 +80,7 @@
78 80
         "lodash": "4.17.21",
79 81
         "moment": "2.29.4",
80 82
         "moment-duration-format": "2.2.2",
83
+        "null-loader": "4.0.1",
81 84
         "optional-require": "1.0.3",
82 85
         "promise.allsettled": "1.0.4",
83 86
         "punycode": "2.1.1",
@@ -124,7 +127,6 @@
124 127
         "redux": "4.0.4",
125 128
         "redux-thunk": "2.2.0",
126 129
         "resemblejs": "4.0.0",
127
-        "rnnoise-wasm": "https://git@github.com/jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
128 130
         "seamless-scroll-polyfill": "2.1.8",
129 131
         "styled-components": "3.4.9",
130 132
         "util": "0.12.1",
@@ -186,6 +188,62 @@
186 188
         "npm": ">=7.0.0"
187 189
       }
188 190
     },
191
+    "../lib-jitsi-meet": {
192
+      "version": "0.0.0",
193
+      "extraneous": true,
194
+      "license": "Apache-2.0",
195
+      "dependencies": {
196
+        "@jitsi/js-utils": "2.0.0",
197
+        "@jitsi/logger": "2.0.0",
198
+        "@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
199
+        "@jitsi/sdp-simulcast": "0.4.0",
200
+        "async": "3.2.3",
201
+        "base64-js": "1.3.1",
202
+        "current-executing-script": "0.1.3",
203
+        "lodash.clonedeep": "4.5.0",
204
+        "lodash.debounce": "4.0.8",
205
+        "lodash.isequal": "4.5.0",
206
+        "promise.allsettled": "1.0.4",
207
+        "sdp-transform": "2.3.0",
208
+        "strophe.js": "1.3.4",
209
+        "strophejs-plugin-disco": "0.0.2",
210
+        "strophejs-plugin-stream-management": "https://git@github.com/jitsi/strophejs-plugin-stream-management#001cf02bef2357234e1ac5d163611b4d60bf2b6a",
211
+        "uuid": "8.1.0",
212
+        "webrtc-adapter": "8.0.0"
213
+      },
214
+      "devDependencies": {
215
+        "@babel/core": "7.16.0",
216
+        "@babel/eslint-parser": "7.16.0",
217
+        "@babel/preset-env": "7.16.0",
218
+        "@babel/preset-typescript": "7.16.7",
219
+        "@jitsi/eslint-config": "4.0.0",
220
+        "@types/async": "3.2.12",
221
+        "@types/jasmine": "3.10.3",
222
+        "@types/sdp-transform": "2.4.5",
223
+        "babel-loader": "8.2.3",
224
+        "core-js": "3.19.1",
225
+        "eslint": "8.1.0",
226
+        "eslint-plugin-import": "2.25.2",
227
+        "jasmine-core": "3.5.0",
228
+        "karma": "6.3.16",
229
+        "karma-chrome-launcher": "3.1.0",
230
+        "karma-jasmine": "3.1.1",
231
+        "karma-sourcemap-loader": "0.3.7",
232
+        "karma-webpack": "5.0.0",
233
+        "process": "0.11.10",
234
+        "string-replace-loader": "3.0.3",
235
+        "typescript": "4.3.5",
236
+        "webpack": "5.57.1",
237
+        "webpack-bundle-analyzer": "4.4.2",
238
+        "webpack-cli": "4.9.0"
239
+      }
240
+    },
241
+    "../rnnoise-wasm": {
242
+      "name": "@jitsi/rnnoise-wasm",
243
+      "version": "0.1.0",
244
+      "extraneous": true,
245
+      "devDependencies": {}
246
+    },
189 247
     "node_modules/@amplitude/react-native": {
190 248
       "version": "2.7.0",
191 249
       "resolved": "https://registry.npmjs.org/@amplitude/react-native/-/react-native-2.7.0.tgz",
@@ -3556,6 +3614,11 @@
3556 3614
       "resolved": "https://registry.npmjs.org/@jitsi/logger/-/logger-2.0.0.tgz",
3557 3615
       "integrity": "sha512-QZE0NpI/GKRdZK0vhuyFYWr4XkCz4slihkSfy6RTszjj/YEHZKIV7yGJo6Hbs3kYI2h5v7apoy+h2WCOMumPJw=="
3558 3616
     },
3617
+    "node_modules/@jitsi/rnnoise-wasm": {
3618
+      "version": "0.1.0",
3619
+      "resolved": "https://registry.npmjs.org/@jitsi/rnnoise-wasm/-/rnnoise-wasm-0.1.0.tgz",
3620
+      "integrity": "sha512-JujivPbOUvdRYa2xqByHYKfKGNGa7ZPyNLaNuh8hEp9XsiNfjaJAHdboq6M1VY9TP+765nyxC0LjpAw1VkikOQ=="
3621
+    },
3559 3622
     "node_modules/@jitsi/rtcstats": {
3560 3623
       "version": "9.2.0",
3561 3624
       "resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.2.0.tgz",
@@ -5316,6 +5379,11 @@
5316 5379
         "node": ">=10.13.0"
5317 5380
       }
5318 5381
     },
5382
+    "node_modules/@types/audioworklet": {
5383
+      "version": "0.0.29",
5384
+      "resolved": "https://registry.npmjs.org/@types/audioworklet/-/audioworklet-0.0.29.tgz",
5385
+      "integrity": "sha512-wNc0CgKOKOIsAf8kH7ICn76H+Zp9GlR5FdP3PXMLcMtSAQdHDaKM3ESVQX9ueTyNm1/UfJCGlcDsN5NdwByrOQ=="
5386
+    },
5319 5387
     "node_modules/@types/body-parser": {
5320 5388
       "version": "1.19.2",
5321 5389
       "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@@ -5475,8 +5543,7 @@
5475 5543
     "node_modules/@types/json-schema": {
5476 5544
       "version": "7.0.9",
5477 5545
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
5478
-      "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
5479
-      "dev": true
5546
+      "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
5480 5547
     },
5481 5548
     "node_modules/@types/json5": {
5482 5549
       "version": "0.0.29",
@@ -5633,9 +5700,9 @@
5633 5700
       "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg=="
5634 5701
     },
5635 5702
     "node_modules/@types/ws": {
5636
-      "version": "8.2.3",
5637
-      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.3.tgz",
5638
-      "integrity": "sha512-ahRJZquUYCdOZf/rCsWg88S0/+cb9wazUBHv6HZEe3XdYaBe2zr/slM8J28X07Hn88Pnm4ezo7N8/ofnOgrPVQ==",
5703
+      "version": "8.5.3",
5704
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
5705
+      "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
5639 5706
       "dev": true,
5640 5707
       "dependencies": {
5641 5708
         "@types/node": "*"
@@ -6512,7 +6579,6 @@
6512 6579
       "version": "6.12.6",
6513 6580
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
6514 6581
       "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
6515
-      "dev": true,
6516 6582
       "dependencies": {
6517 6583
         "fast-deep-equal": "^3.1.1",
6518 6584
         "fast-json-stable-stringify": "^2.0.0",
@@ -6567,7 +6633,6 @@
6567 6633
       "version": "3.5.2",
6568 6634
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
6569 6635
       "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
6570
-      "dev": true,
6571 6636
       "peerDependencies": {
6572 6637
         "ajv": "^6.9.1"
6573 6638
       }
@@ -7310,7 +7375,7 @@
7310 7375
     "node_modules/bonjour": {
7311 7376
       "version": "3.5.0",
7312 7377
       "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
7313
-      "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
7378
+      "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==",
7314 7379
       "dev": true,
7315 7380
       "dependencies": {
7316 7381
         "array-flatten": "^2.1.0",
@@ -8454,7 +8519,7 @@
8454 8519
     "node_modules/current-executing-script": {
8455 8520
       "version": "0.1.3",
8456 8521
       "resolved": "https://registry.npmjs.org/current-executing-script/-/current-executing-script-0.1.3.tgz",
8457
-      "integrity": "sha1-t5jfxYtc+LAPsEwd8KwmY5Z+LHA="
8522
+      "integrity": "sha512-j1nG9I8jaHWniUxJGYkjF3jS98a/mU8tC971XJdrLXKRKSnwNgztd7pHElwdcfJwbQHvJeC9HhUz9NFE8or92g=="
8458 8523
     },
8459 8524
     "node_modules/dayjs": {
8460 8525
       "version": "1.11.1",
@@ -8672,9 +8737,9 @@
8672 8737
       }
8673 8738
     },
8674 8739
     "node_modules/del": {
8675
-      "version": "6.0.0",
8676
-      "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
8677
-      "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
8740
+      "version": "6.1.1",
8741
+      "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
8742
+      "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
8678 8743
       "dev": true,
8679 8744
       "dependencies": {
8680 8745
         "globby": "^11.0.1",
@@ -8779,7 +8844,7 @@
8779 8844
     "node_modules/dns-equal": {
8780 8845
       "version": "1.0.0",
8781 8846
       "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
8782
-      "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
8847
+      "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==",
8783 8848
       "dev": true
8784 8849
     },
8785 8850
     "node_modules/dns-packet": {
@@ -8795,7 +8860,7 @@
8795 8860
     "node_modules/dns-txt": {
8796 8861
       "version": "2.0.2",
8797 8862
       "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
8798
-      "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
8863
+      "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==",
8799 8864
       "dev": true,
8800 8865
       "dependencies": {
8801 8866
         "buffer-indexof": "^1.0.0"
@@ -10186,8 +10251,7 @@
10186 10251
     "node_modules/fast-json-stable-stringify": {
10187 10252
       "version": "2.1.0",
10188 10253
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
10189
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
10190
-      "dev": true
10254
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
10191 10255
     },
10192 10256
     "node_modules/fast-levenshtein": {
10193 10257
       "version": "2.0.6",
@@ -12549,8 +12613,7 @@
12549 12613
     "node_modules/json-schema-traverse": {
12550 12614
       "version": "0.4.1",
12551 12615
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
12552
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
12553
-      "dev": true
12616
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
12554 12617
     },
12555 12618
     "node_modules/json-stable-stringify-without-jsonify": {
12556 12619
       "version": "1.0.1",
@@ -12871,7 +12934,7 @@
12871 12934
     "node_modules/lodash.clonedeep": {
12872 12935
       "version": "4.5.0",
12873 12936
       "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
12874
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
12937
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
12875 12938
     },
12876 12939
     "node_modules/lodash.debounce": {
12877 12940
       "version": "4.0.8",
@@ -12886,7 +12949,7 @@
12886 12949
     "node_modules/lodash.isequal": {
12887 12950
       "version": "4.5.0",
12888 12951
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
12889
-      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
12952
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
12890 12953
     },
12891 12954
     "node_modules/lodash.isstring": {
12892 12955
       "version": "4.0.1",
@@ -13770,7 +13833,7 @@
13770 13833
     "node_modules/multicast-dns-service-types": {
13771 13834
       "version": "1.1.0",
13772 13835
       "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
13773
-      "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
13836
+      "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
13774 13837
       "dev": true
13775 13838
     },
13776 13839
     "node_modules/nan": {
@@ -13912,9 +13975,9 @@
13912 13975
       }
13913 13976
     },
13914 13977
     "node_modules/node-forge": {
13915
-      "version": "1.3.0",
13916
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
13917
-      "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
13978
+      "version": "1.3.1",
13979
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
13980
+      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
13918 13981
       "dev": true,
13919 13982
       "engines": {
13920 13983
         "node": ">= 6.13.0"
@@ -14020,6 +14083,55 @@
14020 14083
         "boolbase": "~1.0.0"
14021 14084
       }
14022 14085
     },
14086
+    "node_modules/null-loader": {
14087
+      "version": "4.0.1",
14088
+      "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz",
14089
+      "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==",
14090
+      "dependencies": {
14091
+        "loader-utils": "^2.0.0",
14092
+        "schema-utils": "^3.0.0"
14093
+      },
14094
+      "engines": {
14095
+        "node": ">= 10.13.0"
14096
+      },
14097
+      "funding": {
14098
+        "type": "opencollective",
14099
+        "url": "https://opencollective.com/webpack"
14100
+      },
14101
+      "peerDependencies": {
14102
+        "webpack": "^4.0.0 || ^5.0.0"
14103
+      }
14104
+    },
14105
+    "node_modules/null-loader/node_modules/loader-utils": {
14106
+      "version": "2.0.2",
14107
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
14108
+      "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
14109
+      "dependencies": {
14110
+        "big.js": "^5.2.2",
14111
+        "emojis-list": "^3.0.0",
14112
+        "json5": "^2.1.2"
14113
+      },
14114
+      "engines": {
14115
+        "node": ">=8.9.0"
14116
+      }
14117
+    },
14118
+    "node_modules/null-loader/node_modules/schema-utils": {
14119
+      "version": "3.1.1",
14120
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
14121
+      "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
14122
+      "dependencies": {
14123
+        "@types/json-schema": "^7.0.8",
14124
+        "ajv": "^6.12.5",
14125
+        "ajv-keywords": "^3.5.2"
14126
+      },
14127
+      "engines": {
14128
+        "node": ">= 10.13.0"
14129
+      },
14130
+      "funding": {
14131
+        "type": "opencollective",
14132
+        "url": "https://opencollective.com/webpack"
14133
+      }
14134
+    },
14023 14135
     "node_modules/nullthrows": {
14024 14136
       "version": "1.1.1",
14025 14137
       "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -17313,11 +17425,6 @@
17313 17425
         "url": "https://github.com/sponsors/isaacs"
17314 17426
       }
17315 17427
     },
17316
-    "node_modules/rnnoise-wasm": {
17317
-      "version": "0.0.1",
17318
-      "resolved": "git+https://git@github.com/jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
17319
-      "integrity": "sha512-XQgO7DDtjsXzaHU4WiahPrmoU2BmfuT0/0dexNoufSid+fVuTlsXPpZxHq+aSk0/7idvtbO8Xru1khMRv1dPWw=="
17320
-    },
17321 17428
     "node_modules/rtl-css-js": {
17322 17429
       "version": "1.15.0",
17323 17430
       "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.15.0.tgz",
@@ -17433,7 +17540,7 @@
17433 17540
     "node_modules/sdp-transform": {
17434 17541
       "version": "2.3.0",
17435 17542
       "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz",
17436
-      "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY=",
17543
+      "integrity": "sha512-zR0e9ciWFezeaKLLpWCrOCiYmGIQN9jfO5Ayfs7m5k2/g9b2MEEIvQ/TTmymm167zozTNYSQoLGKDihMoTWkkw==",
17437 17544
       "bin": {
17438 17545
         "sdp-verify": "checker.js"
17439 17546
       }
@@ -17455,12 +17562,12 @@
17455 17562
       "dev": true
17456 17563
     },
17457 17564
     "node_modules/selfsigned": {
17458
-      "version": "2.0.0",
17459
-      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz",
17460
-      "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==",
17565
+      "version": "2.0.1",
17566
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz",
17567
+      "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==",
17461 17568
       "dev": true,
17462 17569
       "dependencies": {
17463
-        "node-forge": "^1.2.0"
17570
+        "node-forge": "^1"
17464 17571
       },
17465 17572
       "engines": {
17466 17573
         "node": ">=10"
@@ -19528,7 +19635,6 @@
19528 19635
       "version": "4.4.1",
19529 19636
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
19530 19637
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
19531
-      "dev": true,
19532 19638
       "dependencies": {
19533 19639
         "punycode": "^2.1.0"
19534 19640
       }
@@ -23325,6 +23431,11 @@
23325 23431
       "resolved": "https://registry.npmjs.org/@jitsi/logger/-/logger-2.0.0.tgz",
23326 23432
       "integrity": "sha512-QZE0NpI/GKRdZK0vhuyFYWr4XkCz4slihkSfy6RTszjj/YEHZKIV7yGJo6Hbs3kYI2h5v7apoy+h2WCOMumPJw=="
23327 23433
     },
23434
+    "@jitsi/rnnoise-wasm": {
23435
+      "version": "0.1.0",
23436
+      "resolved": "https://registry.npmjs.org/@jitsi/rnnoise-wasm/-/rnnoise-wasm-0.1.0.tgz",
23437
+      "integrity": "sha512-JujivPbOUvdRYa2xqByHYKfKGNGa7ZPyNLaNuh8hEp9XsiNfjaJAHdboq6M1VY9TP+765nyxC0LjpAw1VkikOQ=="
23438
+    },
23328 23439
     "@jitsi/rtcstats": {
23329 23440
       "version": "9.2.0",
23330 23441
       "resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.2.0.tgz",
@@ -24596,6 +24707,11 @@
24596 24707
       "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
24597 24708
       "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
24598 24709
     },
24710
+    "@types/audioworklet": {
24711
+      "version": "0.0.29",
24712
+      "resolved": "https://registry.npmjs.org/@types/audioworklet/-/audioworklet-0.0.29.tgz",
24713
+      "integrity": "sha512-wNc0CgKOKOIsAf8kH7ICn76H+Zp9GlR5FdP3PXMLcMtSAQdHDaKM3ESVQX9ueTyNm1/UfJCGlcDsN5NdwByrOQ=="
24714
+    },
24599 24715
     "@types/body-parser": {
24600 24716
       "version": "1.19.2",
24601 24717
       "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@@ -24755,8 +24871,7 @@
24755 24871
     "@types/json-schema": {
24756 24872
       "version": "7.0.9",
24757 24873
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
24758
-      "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
24759
-      "dev": true
24874
+      "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
24760 24875
     },
24761 24876
     "@types/json5": {
24762 24877
       "version": "0.0.29",
@@ -24913,9 +25028,9 @@
24913 25028
       "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg=="
24914 25029
     },
24915 25030
     "@types/ws": {
24916
-      "version": "8.2.3",
24917
-      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.3.tgz",
24918
-      "integrity": "sha512-ahRJZquUYCdOZf/rCsWg88S0/+cb9wazUBHv6HZEe3XdYaBe2zr/slM8J28X07Hn88Pnm4ezo7N8/ofnOgrPVQ==",
25031
+      "version": "8.5.3",
25032
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
25033
+      "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
24919 25034
       "dev": true,
24920 25035
       "requires": {
24921 25036
         "@types/node": "*"
@@ -25519,7 +25634,6 @@
25519 25634
       "version": "6.12.6",
25520 25635
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
25521 25636
       "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
25522
-      "dev": true,
25523 25637
       "requires": {
25524 25638
         "fast-deep-equal": "^3.1.1",
25525 25639
         "fast-json-stable-stringify": "^2.0.0",
@@ -25559,8 +25673,7 @@
25559 25673
     "ajv-keywords": {
25560 25674
       "version": "3.5.2",
25561 25675
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
25562
-      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
25563
-      "dev": true
25676
+      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
25564 25677
     },
25565 25678
     "alphanum-sort": {
25566 25679
       "version": "1.0.2",
@@ -26156,7 +26269,7 @@
26156 26269
     "bonjour": {
26157 26270
       "version": "3.5.0",
26158 26271
       "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
26159
-      "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
26272
+      "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==",
26160 26273
       "dev": true,
26161 26274
       "requires": {
26162 26275
         "array-flatten": "^2.1.0",
@@ -27041,7 +27154,7 @@
27041 27154
     "current-executing-script": {
27042 27155
       "version": "0.1.3",
27043 27156
       "resolved": "https://registry.npmjs.org/current-executing-script/-/current-executing-script-0.1.3.tgz",
27044
-      "integrity": "sha1-t5jfxYtc+LAPsEwd8KwmY5Z+LHA="
27157
+      "integrity": "sha512-j1nG9I8jaHWniUxJGYkjF3jS98a/mU8tC971XJdrLXKRKSnwNgztd7pHElwdcfJwbQHvJeC9HhUz9NFE8or92g=="
27045 27158
     },
27046 27159
     "dayjs": {
27047 27160
       "version": "1.11.1",
@@ -27196,9 +27309,9 @@
27196 27309
       }
27197 27310
     },
27198 27311
     "del": {
27199
-      "version": "6.0.0",
27200
-      "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
27201
-      "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
27312
+      "version": "6.1.1",
27313
+      "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
27314
+      "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
27202 27315
       "dev": true,
27203 27316
       "requires": {
27204 27317
         "globby": "^11.0.1",
@@ -27284,7 +27397,7 @@
27284 27397
     "dns-equal": {
27285 27398
       "version": "1.0.0",
27286 27399
       "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
27287
-      "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
27400
+      "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==",
27288 27401
       "dev": true
27289 27402
     },
27290 27403
     "dns-packet": {
@@ -27300,7 +27413,7 @@
27300 27413
     "dns-txt": {
27301 27414
       "version": "2.0.2",
27302 27415
       "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
27303
-      "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
27416
+      "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==",
27304 27417
       "dev": true,
27305 27418
       "requires": {
27306 27419
         "buffer-indexof": "^1.0.0"
@@ -28382,8 +28495,7 @@
28382 28495
     "fast-json-stable-stringify": {
28383 28496
       "version": "2.1.0",
28384 28497
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
28385
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
28386
-      "dev": true
28498
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
28387 28499
     },
28388 28500
     "fast-levenshtein": {
28389 28501
       "version": "2.0.6",
@@ -30171,8 +30283,7 @@
30171 30283
     "json-schema-traverse": {
30172 30284
       "version": "0.4.1",
30173 30285
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
30174
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
30175
-      "dev": true
30286
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
30176 30287
     },
30177 30288
     "json-stable-stringify-without-jsonify": {
30178 30289
       "version": "1.0.1",
@@ -30446,7 +30557,7 @@
30446 30557
     "lodash.clonedeep": {
30447 30558
       "version": "4.5.0",
30448 30559
       "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
30449
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
30560
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
30450 30561
     },
30451 30562
     "lodash.debounce": {
30452 30563
       "version": "4.0.8",
@@ -30461,7 +30572,7 @@
30461 30572
     "lodash.isequal": {
30462 30573
       "version": "4.5.0",
30463 30574
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
30464
-      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
30575
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
30465 30576
     },
30466 30577
     "lodash.isstring": {
30467 30578
       "version": "4.0.1",
@@ -31180,7 +31291,7 @@
31180 31291
     "multicast-dns-service-types": {
31181 31292
       "version": "1.1.0",
31182 31293
       "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
31183
-      "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
31294
+      "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
31184 31295
       "dev": true
31185 31296
     },
31186 31297
     "nan": {
@@ -31290,9 +31401,9 @@
31290 31401
       }
31291 31402
     },
31292 31403
     "node-forge": {
31293
-      "version": "1.3.0",
31294
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
31295
-      "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
31404
+      "version": "1.3.1",
31405
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
31406
+      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
31296 31407
       "dev": true
31297 31408
     },
31298 31409
     "node-int64": {
@@ -31369,6 +31480,37 @@
31369 31480
         "boolbase": "~1.0.0"
31370 31481
       }
31371 31482
     },
31483
+    "null-loader": {
31484
+      "version": "4.0.1",
31485
+      "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz",
31486
+      "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==",
31487
+      "requires": {
31488
+        "loader-utils": "^2.0.0",
31489
+        "schema-utils": "^3.0.0"
31490
+      },
31491
+      "dependencies": {
31492
+        "loader-utils": {
31493
+          "version": "2.0.2",
31494
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
31495
+          "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
31496
+          "requires": {
31497
+            "big.js": "^5.2.2",
31498
+            "emojis-list": "^3.0.0",
31499
+            "json5": "^2.1.2"
31500
+          }
31501
+        },
31502
+        "schema-utils": {
31503
+          "version": "3.1.1",
31504
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
31505
+          "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
31506
+          "requires": {
31507
+            "@types/json-schema": "^7.0.8",
31508
+            "ajv": "^6.12.5",
31509
+            "ajv-keywords": "^3.5.2"
31510
+          }
31511
+        }
31512
+      }
31513
+    },
31372 31514
     "nullthrows": {
31373 31515
       "version": "1.1.1",
31374 31516
       "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -33785,11 +33927,6 @@
33785 33927
         "glob": "^7.1.3"
33786 33928
       }
33787 33929
     },
33788
-    "rnnoise-wasm": {
33789
-      "version": "git+https://git@github.com/jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
33790
-      "integrity": "sha512-XQgO7DDtjsXzaHU4WiahPrmoU2BmfuT0/0dexNoufSid+fVuTlsXPpZxHq+aSk0/7idvtbO8Xru1khMRv1dPWw==",
33791
-      "from": "rnnoise-wasm@https://git@github.com/jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af"
33792
-    },
33793 33930
     "rtl-css-js": {
33794 33931
       "version": "1.15.0",
33795 33932
       "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.15.0.tgz",
@@ -33872,7 +34009,7 @@
33872 34009
     "sdp-transform": {
33873 34010
       "version": "2.3.0",
33874 34011
       "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz",
33875
-      "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY="
34012
+      "integrity": "sha512-zR0e9ciWFezeaKLLpWCrOCiYmGIQN9jfO5Ayfs7m5k2/g9b2MEEIvQ/TTmymm167zozTNYSQoLGKDihMoTWkkw=="
33876 34013
     },
33877 34014
     "seamless-scroll-polyfill": {
33878 34015
       "version": "2.1.8",
@@ -33891,12 +34028,12 @@
33891 34028
       "dev": true
33892 34029
     },
33893 34030
     "selfsigned": {
33894
-      "version": "2.0.0",
33895
-      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz",
33896
-      "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==",
34031
+      "version": "2.0.1",
34032
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz",
34033
+      "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==",
33897 34034
       "dev": true,
33898 34035
       "requires": {
33899
-        "node-forge": "^1.2.0"
34036
+        "node-forge": "^1"
33900 34037
       }
33901 34038
     },
33902 34039
     "semver": {
@@ -35533,7 +35670,6 @@
35533 35670
       "version": "4.4.1",
35534 35671
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
35535 35672
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
35536
-      "dev": true,
35537 35673
       "requires": {
35538 35674
         "punycode": "^2.1.0"
35539 35675
       }

+ 3
- 1
package.json View File

@@ -39,6 +39,7 @@
39 39
     "@hapi/bourne": "2.0.0",
40 40
     "@jitsi/js-utils": "2.0.0",
41 41
     "@jitsi/logger": "2.0.0",
42
+    "@jitsi/rnnoise-wasm": "0.1.0",
42 43
     "@jitsi/rtcstats": "9.2.0",
43 44
     "@material-ui/core": "4.11.3",
44 45
     "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
@@ -60,6 +61,7 @@
60 61
     "@tensorflow/tfjs-core": "3.13.0",
61 62
     "@vladmandic/human": "2.6.5",
62 63
     "@vladmandic/human-models": "2.5.9",
64
+    "@types/audioworklet": "0.0.29",
63 65
     "@xmldom/xmldom": "0.7.5",
64 66
     "amplitude-js": "8.2.1",
65 67
     "base64-js": "1.3.1",
@@ -83,6 +85,7 @@
83 85
     "lodash": "4.17.21",
84 86
     "moment": "2.29.4",
85 87
     "moment-duration-format": "2.2.2",
88
+    "null-loader": "4.0.1",
86 89
     "optional-require": "1.0.3",
87 90
     "promise.allsettled": "1.0.4",
88 91
     "punycode": "2.1.1",
@@ -129,7 +132,6 @@
129 132
     "redux": "4.0.4",
130 133
     "redux-thunk": "2.2.0",
131 134
     "resemblejs": "4.0.0",
132
-    "rnnoise-wasm": "https://git@github.com/jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
133 135
     "seamless-scroll-polyfill": "2.1.8",
134 136
     "styled-components": "3.4.9",
135 137
     "util": "0.12.1",

+ 1
- 0
react/features/app/reducers.web.js View File

@@ -11,6 +11,7 @@ import '../power-monitor/reducer';
11 11
 import '../prejoin/reducer';
12 12
 import '../remote-control/reducer';
13 13
 import '../screen-share/reducer';
14
+import '../noise-suppression/reducer';
14 15
 import '../screenshot-capture/reducer';
15 16
 import '../shared-video/reducer';
16 17
 import '../talk-while-muted/reducer';

+ 3
- 0
react/features/app/types.ts View File

@@ -12,6 +12,8 @@ import { IFlagsState } from '../base/flags/reducer';
12 12
 import { IJwtState } from '../base/jwt/reducer';
13 13
 import { ILastNState } from '../base/lastn/reducer';
14 14
 import { ILibJitsiMeetState } from '../base/lib-jitsi-meet/reducer';
15
+import { INoiseSuppressionState } from '../noise-suppression/reducer';
16
+
15 17
 
16 18
 export interface IStore {
17 19
     dispatch: Function,
@@ -34,4 +36,5 @@ export interface IState {
34 36
     'features/base/known-domains': Array<string>,
35 37
     'features/base/lastn': ILastNState,
36 38
     'features/base/lib-jitsi-meet': ILibJitsiMeetState
39
+    'features/noise-suppression': INoiseSuppressionState
37 40
 }

+ 5
- 0
react/features/base/conference/middleware.web.js View File

@@ -1,6 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
4
+import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
4 5
 import { showNotification, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications';
5 6
 import {
6 7
     setPrejoinPageVisibility,
@@ -169,6 +170,10 @@ async function _toggleScreenSharing({ enabled, audioOnly = false }, store) {
169 170
         // Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference
170 171
         // otherwise without unmuting the microphone.
171 172
         if (desktopAudioTrack) {
173
+            // Noise suppression doesn't work with desktop audio because we can't chain
174
+            // track effects yet, disable it first.
175
+            // We need to to wait for the effect to clear first or it might interfere with the audio mixer.
176
+            await dispatch(setNoiseSuppressionEnabled(false));
172 177
             _maybeApplyAudioMixerEffect(desktopAudioTrack, state);
173 178
             dispatch(setScreenshareAudioTrack(desktopAudioTrack));
174 179
         }

+ 1
- 0
react/features/base/config/constants.js View File

@@ -41,6 +41,7 @@ export const TOOLBAR_BUTTONS = [
41 41
     'select-background',
42 42
     'settings',
43 43
     'shareaudio',
44
+    'noisesuppression',
44 45
     'sharedvideo',
45 46
     'shortcuts',
46 47
     'stats',

+ 37
- 0
react/features/base/util/math.ts View File

@@ -0,0 +1,37 @@
1
+/**
2
+ * Compute the greatest common divisor using Euclid's algorithm.
3
+ *
4
+ * @param {number} num1 - First number.
5
+ * @param {number} num2 - Second number.
6
+ * @returns {number}
7
+ */
8
+export function greatestCommonDivisor(num1: number, num2: number) {
9
+    let number1: number = num1;
10
+    let number2: number = num2;
11
+
12
+    while (number1 !== number2) {
13
+        if (number1 > number2) {
14
+            number1 = number1 - number2;
15
+        } else {
16
+            number2 = number2 - number1;
17
+        }
18
+    }
19
+
20
+    return number2;
21
+}
22
+
23
+/**
24
+ * Calculate least common multiple using gcd.
25
+ *
26
+ * @param {number} num1 - First number.
27
+ * @param {number} num2 - Second number.
28
+ * @returns {number}
29
+ */
30
+export function leastCommonMultiple(num1: number, num2: number) {
31
+    const number1: number = num1;
32
+    const number2: number = num2;
33
+
34
+    const gcd: number = greatestCommonDivisor(number1, number2);
35
+
36
+    return (number1 * number2) / gcd;
37
+}

+ 9
- 0
react/features/noise-suppression/actionTypes.ts View File

@@ -0,0 +1,9 @@
1
+/**
2
+ * Type of action which sets the current state of noise suppression.
3
+ *
4
+ * {
5
+ *     type: SET_NOISE_SUPPRESSION_ENABLED,
6
+ *     enabled: boolean
7
+ * }
8
+ */
9
+export const SET_NOISE_SUPPRESSION_ENABLED = 'SET_NOISE_SUPPRESSION_ENABLED';

+ 100
- 0
react/features/noise-suppression/actions.ts View File

@@ -0,0 +1,100 @@
1
+/* eslint-disable lines-around-comment */
2
+import { Dispatch } from 'redux';
3
+
4
+// @ts-ignore
5
+import { getLocalJitsiAudioTrack } from '../base/tracks';
6
+// @ts-ignore
7
+import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showWarningNotification } from '../notifications';
8
+// @ts-ignore
9
+import { NoiseSuppressionEffect } from '../stream-effects/noise-suppression/NoiseSuppressionEffect';
10
+
11
+import { SET_NOISE_SUPPRESSION_ENABLED } from './actionTypes';
12
+import { canEnableNoiseSuppression, isNoiseSuppressionEnabled } from './functions';
13
+import logger from './logger';
14
+
15
+/**
16
+ * Updates the noise suppression active state.
17
+ *
18
+ * @param {boolean} enabled - Is noise suppression enabled.
19
+ * @returns {{
20
+ *      type: SET_NOISE_SUPPRESSION_STATE,
21
+ *      enabled: boolean
22
+ * }}
23
+ */
24
+export function setNoiseSuppressionEnabledState(enabled: boolean) : any {
25
+    return {
26
+        type: SET_NOISE_SUPPRESSION_ENABLED,
27
+        enabled
28
+    };
29
+}
30
+
31
+/**
32
+ *  Enabled/disable noise suppression depending on the current state.
33
+ *
34
+ * @returns {Function}
35
+ */
36
+export function toggleNoiseSuppression() : any {
37
+    return (dispatch: Dispatch, getState: Function) => {
38
+        if (isNoiseSuppressionEnabled(getState())) {
39
+            dispatch(setNoiseSuppressionEnabled(false));
40
+        } else {
41
+            dispatch(setNoiseSuppressionEnabled(true));
42
+        }
43
+    };
44
+}
45
+
46
+/**
47
+ * Attempt to enable or disable noise suppression using the {@link NoiseSuppressionEffect}.
48
+ *
49
+ * @param {boolean} enabled - Enable or disable noise suppression.
50
+ *
51
+ * @returns {Function}
52
+ */
53
+export function setNoiseSuppressionEnabled(enabled: boolean) : any {
54
+    return async (dispatch: Dispatch, getState: Function) => {
55
+        const state = getState();
56
+
57
+        const localAudio = getLocalJitsiAudioTrack(state);
58
+        const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
59
+
60
+        logger.info(`Attempting to set noise suppression enabled state: ${enabled}`);
61
+
62
+        if (!localAudio) {
63
+            logger.warn('Can not apply noise suppression without any local track active.');
64
+
65
+            dispatch(showWarningNotification({
66
+                titleKey: 'notify.noiseSuppressionFailedTitle',
67
+                descriptionKey: 'notify.noiseSuppressionNoTrackDescription'
68
+            }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
69
+
70
+            return;
71
+        }
72
+        try {
73
+            if (enabled && !noiseSuppressionEnabled) {
74
+                if (!canEnableNoiseSuppression(state, dispatch, localAudio)) {
75
+                    return;
76
+                }
77
+
78
+                await localAudio.setEffect(new NoiseSuppressionEffect());
79
+                dispatch(setNoiseSuppressionEnabledState(true));
80
+                logger.info('Noise suppression enabled.');
81
+
82
+            } else if (!enabled && noiseSuppressionEnabled) {
83
+                await localAudio.setEffect(undefined);
84
+                dispatch(setNoiseSuppressionEnabledState(false));
85
+                logger.info('Noise suppression disabled.');
86
+            } else {
87
+                logger.warn(`Noise suppression enabled state already: ${enabled}`);
88
+            }
89
+        } catch (error) {
90
+            logger.error(
91
+                `Failed to set noise suppression enabled to: ${enabled}`,
92
+                error
93
+            );
94
+
95
+            dispatch(showErrorNotification({
96
+                titleKey: 'notify.noiseSuppressionFailedTitle'
97
+            }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
98
+        }
99
+    };
100
+}

+ 84
- 0
react/features/noise-suppression/components/NoiseSuppressionButton.tsx View File

@@ -0,0 +1,84 @@
1
+/* eslint-disable lines-around-comment */
2
+import { IState } from '../../app/types';
3
+// @ts-ignore
4
+import { translate } from '../../base/i18n';
5
+// @ts-ignore
6
+import {
7
+    IconShareAudio,
8
+    IconStopAudioShare
9
+    // @ts-ignore
10
+} from '../../base/icons';
11
+// @ts-ignore
12
+import { connect } from '../../base/redux';
13
+// @ts-ignore
14
+import {
15
+    AbstractButton,
16
+    type AbstractButtonProps
17
+    // @ts-ignore
18
+} from '../../base/toolbox/components';
19
+// @ts-ignore
20
+import { setOverflowMenuVisible } from '../../toolbox/actions';
21
+import { toggleNoiseSuppression } from '../actions';
22
+import { isNoiseSuppressionEnabled } from '../functions';
23
+
24
+type Props = AbstractButtonProps & {
25
+
26
+    /**
27
+     * The redux {@code dispatch} function.
28
+     */
29
+    dispatch: Function;
30
+
31
+}
32
+
33
+/**
34
+ * Component that renders a toolbar button for toggling noise suppression.
35
+ */
36
+class NoiseSuppressionButton extends AbstractButton<Props, any, any> {
37
+    accessibilityLabel = 'toolbar.accessibilityLabel.noiseSuppression';
38
+    icon = IconShareAudio;
39
+    label = 'toolbar.noiseSuppression';
40
+    tooltip = 'toolbar.noiseSuppression';
41
+    toggledIcon = IconStopAudioShare;
42
+    toggledLabel = 'toolbar.disableNoiseSuppression';
43
+
44
+    private props: Props;
45
+
46
+    /**
47
+     * Handles clicking / pressing the button.
48
+     *
49
+     * @private
50
+     * @returns {void}
51
+     */
52
+    _handleClick() {
53
+        const { dispatch } = this.props;
54
+
55
+        dispatch(toggleNoiseSuppression());
56
+        dispatch(setOverflowMenuVisible(false));
57
+    }
58
+
59
+    /**
60
+     * Indicates whether this button is in toggled state or not.
61
+     *
62
+     * @override
63
+     * @protected
64
+     * @returns {boolean}
65
+     */
66
+    _isToggled() {
67
+        return this.props._isNoiseSuppressionEnabled;
68
+    }
69
+}
70
+
71
+/**
72
+ * Maps part of the Redux state to the props of this component.
73
+ *
74
+ * @param {Object} state - The Redux state.
75
+ * @private
76
+ * @returns {Props}
77
+ */
78
+function _mapStateToProps(state: IState): Object {
79
+    return {
80
+        _isNoiseSuppressionEnabled: isNoiseSuppressionEnabled(state)
81
+    };
82
+}
83
+
84
+export default translate(connect(_mapStateToProps)(NoiseSuppressionButton));

+ 1
- 0
react/features/noise-suppression/components/index.ts View File

@@ -0,0 +1 @@
1
+export { default as NoiseSuppressionButton } from './NoiseSuppressionButton';

+ 51
- 0
react/features/noise-suppression/functions.ts View File

@@ -0,0 +1,51 @@
1
+/* eslint-disable lines-around-comment */
2
+import { IState } from '../app/types';
3
+// @ts-ignore
4
+import { NOTIFICATION_TIMEOUT_TYPE, showWarningNotification } from '../notifications';
5
+// @ts-ignore
6
+import { isScreenAudioShared } from '../screen-share';
7
+
8
+/**
9
+ * Is noise suppression currently enabled.
10
+ *
11
+ * @param {IState} state - The state of the application.
12
+ * @returns {boolean}
13
+ */
14
+export function isNoiseSuppressionEnabled(state: IState): boolean {
15
+    return state['features/noise-suppression'].enabled;
16
+}
17
+
18
+/**
19
+ * Verify if noise suppression can be enabled in the current state.
20
+ *
21
+ * @param {*} state - Redux state.
22
+ * @param {*} dispatch - Redux dispatch.
23
+ * @param {*} localAudio - Current local audio track.
24
+ * @returns {boolean}
25
+ */
26
+export function canEnableNoiseSuppression(state: IState, dispatch: Function, localAudio: any) : boolean {
27
+    const { channelCount } = localAudio.track.getSettings();
28
+
29
+    // Sharing screen audio implies an effect being applied to the local track, because currently we don't support
30
+    // more then one effect at a time the user has to choose between sharing audio or having noise suppression active.
31
+    if (isScreenAudioShared(state)) {
32
+        dispatch(showWarningNotification({
33
+            titleKey: 'notify.noiseSuppressionFailedTitle',
34
+            descriptionKey: 'notify.noiseSuppressionDesktopAudioDescription'
35
+        }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
36
+
37
+        return false;
38
+    }
39
+
40
+    // Stereo audio tracks aren't currently supported, make sure the current local track is mono
41
+    if (channelCount > 1) {
42
+        dispatch(showWarningNotification({
43
+            titleKey: 'notify.noiseSuppressionFailedTitle',
44
+            descriptionKey: 'notify.noiseSuppressionStereoDescription'
45
+        }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
46
+
47
+        return false;
48
+    }
49
+
50
+    return true;
51
+}

+ 4
- 0
react/features/noise-suppression/logger.ts View File

@@ -0,0 +1,4 @@
1
+// @ts-ignore
2
+import { getLogger } from '../base/logging/functions';
3
+
4
+export default getLogger('features/noise-suppression');

+ 31
- 0
react/features/noise-suppression/reducer.ts View File

@@ -0,0 +1,31 @@
1
+// @ts-ignore
2
+import { ReducerRegistry } from '../base/redux';
3
+
4
+import {
5
+    SET_NOISE_SUPPRESSION_ENABLED
6
+} from './actionTypes';
7
+
8
+export interface INoiseSuppressionState {
9
+    enabled: boolean
10
+}
11
+
12
+const DEFAULT_STATE = {
13
+    enabled: false
14
+};
15
+
16
+/**
17
+ * Reduces the Redux actions of the feature features/noise-suppression.
18
+ */
19
+ReducerRegistry.register('features/noise-suppression', (state: INoiseSuppressionState = DEFAULT_STATE, action: any) => {
20
+    const { enabled } = action;
21
+
22
+    switch (action.type) {
23
+    case SET_NOISE_SUPPRESSION_ENABLED:
24
+        return {
25
+            ...state,
26
+            enabled
27
+        };
28
+    default:
29
+        return state;
30
+    }
31
+});

+ 89
- 0
react/features/stream-effects/noise-suppression/NoiseSuppressionEffect.ts View File

@@ -0,0 +1,89 @@
1
+// @ts-ignore
2
+import { getBaseUrl } from '../../base/util';
3
+
4
+import logger from './logger';
5
+
6
+/**
7
+ * Class Implementing the effect interface expected by a JitsiLocalTrack.
8
+ * Effect applies rnnoise denoising on a audio JitsiLocalTrack.
9
+ */
10
+export class NoiseSuppressionEffect {
11
+
12
+    /**
13
+     * Web audio context.
14
+     */
15
+    private _audioContext: AudioContext;
16
+
17
+    /**
18
+     * Source that will be attached to the track affected by the effect.
19
+     */
20
+    private _audioSource: MediaStreamAudioSourceNode;
21
+
22
+    /**
23
+     * Destination that will contain denoised audio from the audio worklet.
24
+     */
25
+    private _audioDestination: MediaStreamAudioDestinationNode;
26
+
27
+    /**
28
+     * `AudioWorkletProcessor` associated node.
29
+     */
30
+    private _noiseSuppressorNode: AudioWorkletNode;
31
+
32
+    /**
33
+     * Effect interface called by source JitsiLocalTrack.
34
+     * Applies effect that uses a {@code NoiseSuppressor} service initialized with {@code RnnoiseProcessor}
35
+     * for denoising.
36
+     *
37
+     * @param {MediaStream} audioStream - Audio stream which will be mixed with _mixAudio.
38
+     * @returns {MediaStream} - MediaStream containing both audio tracks mixed together.
39
+     */
40
+    startEffect(audioStream: MediaStream) : MediaStream {
41
+        this._audioContext = new AudioContext();
42
+
43
+        this._audioSource = this._audioContext.createMediaStreamSource(audioStream);
44
+        this._audioDestination = this._audioContext.createMediaStreamDestination();
45
+
46
+        const baseUrl = `${getBaseUrl()}libs/`;
47
+        const workletUrl = `${baseUrl}noise-suppressor-worklet.min.js`;
48
+
49
+        // Connect the audio processing graph MediaStream -> AudioWorkletNode -> MediaStreamAudioDestinationNode
50
+        this._audioContext.audioWorklet.addModule(workletUrl)
51
+        .then(() => {
52
+            // After the resolution of module loading, an AudioWorkletNode can be constructed.
53
+            this._noiseSuppressorNode = new AudioWorkletNode(this._audioContext, 'NoiseSuppressorWorklet');
54
+            this._audioSource.connect(this._noiseSuppressorNode).connect(this._audioDestination);
55
+        })
56
+        .catch(error => {
57
+            logger.error('Error while adding audio worklet module: ', error);
58
+        });
59
+
60
+        return this._audioDestination.stream;
61
+    }
62
+
63
+    /**
64
+     * Checks if the JitsiLocalTrack supports this effect.
65
+     *
66
+     * @param {JitsiLocalTrack} sourceLocalTrack - Track to which the effect will be applied.
67
+     * @returns {boolean} - Returns true if this effect can run on the specified track, false otherwise.
68
+     */
69
+    isEnabled(sourceLocalTrack: any): boolean {
70
+        // JitsiLocalTracks needs to be an audio track.
71
+        return sourceLocalTrack.isAudioTrack();
72
+    }
73
+
74
+    /**
75
+     * Clean up resources acquired by noise suppressor and rnnoise processor.
76
+     *
77
+     * @returns {void}
78
+     */
79
+    stopEffect(): void {
80
+        // Technically after this process the Audio Worklet along with it's resources should be garbage collected,
81
+        // however on chrome there seems to be a problem as described here:
82
+        // https://bugs.chromium.org/p/chromium/issues/detail?id=1298955
83
+        this._noiseSuppressorNode?.port?.close();
84
+        this._audioDestination?.disconnect();
85
+        this._noiseSuppressorNode?.disconnect();
86
+        this._audioSource?.disconnect();
87
+        this._audioContext?.close();
88
+    }
89
+}

+ 171
- 0
react/features/stream-effects/noise-suppression/NoiseSuppressorWorklet.ts View File

@@ -0,0 +1,171 @@
1
+// @ts-ignore
2
+import { createRNNWasmModuleSync } from '@jitsi/rnnoise-wasm';
3
+
4
+import { leastCommonMultiple } from '../../base/util/math';
5
+import RnnoiseProcessor from '../rnnoise/RnnoiseProcessor';
6
+
7
+
8
+/**
9
+ * Audio worklet which will denoise targeted audio stream using rnnoise.
10
+ */
11
+class NoiseSuppressorWorklet extends AudioWorkletProcessor {
12
+    /**
13
+     * RnnoiseProcessor instance.
14
+     */
15
+    private _denoiseProcessor: RnnoiseProcessor;
16
+
17
+    /**
18
+     * Audio worklets work with a predefined sample rate of 128.
19
+     */
20
+    private _procNodeSampleRate = 128;
21
+
22
+    /**
23
+     * PCM Sample size expected by the denoise processor.
24
+     */
25
+    private _denoiseSampleSize: number;
26
+
27
+    /**
28
+     * Circular buffer data used for efficient memory operations.
29
+     */
30
+    private _circularBufferLength: number;
31
+
32
+    private _circularBuffer: Float32Array;
33
+
34
+    /**
35
+     * The circular buffer uses a couple of indexes to track data segments. Input data from the stream is
36
+     * copied to the circular buffer as it comes in, one `procNodeSampleRate` sized sample at a time.
37
+     * _inputBufferLength denotes the current length of all gathered raw audio segments.
38
+     */
39
+    private _inputBufferLength = 0;
40
+
41
+    /**
42
+     * Denoising is done directly on the circular buffer using subArray views, but because
43
+     * `procNodeSampleRate` and `_denoiseSampleSize` have different sizes, denoised samples lag behind
44
+     * the current gathered raw audio samples so we need a different index, `_denoisedBufferLength`.
45
+     */
46
+    private _denoisedBufferLength = 0;
47
+
48
+    /**
49
+     * Once enough data has been denoised (size of procNodeSampleRate) it's sent to the
50
+     * output buffer, `_denoisedBufferIndx` indicates the start index on the circular buffer
51
+     * of denoised data not yet sent.
52
+     */
53
+    private _denoisedBufferIndx = 0;
54
+
55
+    /**
56
+     * C'tor.
57
+     */
58
+    constructor() {
59
+        super();
60
+
61
+        /**
62
+         * The wasm module needs to be compiled to load synchronously as the audio worklet `addModule()`
63
+         * initialization process does not wait for the resolution of promises in the AudioWorkletGlobalScope.
64
+         */
65
+        this._denoiseProcessor = new RnnoiseProcessor(createRNNWasmModuleSync());
66
+
67
+        /**
68
+         * PCM Sample size expected by the denoise processor.
69
+         */
70
+        this._denoiseSampleSize = this._denoiseProcessor.getSampleLength();
71
+
72
+        /**
73
+         * In order to avoid unnecessary memory related operations a circular buffer was used.
74
+         * Because the audio worklet input array does not match the sample size required by rnnoise two cases can occur
75
+         * 1. There is not enough data in which case we buffer it.
76
+         * 2. There is enough data but some residue remains after the call to `processAudioFrame`, so its buffered
77
+         * for the next call.
78
+         * A problem arises when the circular buffer reaches the end and a rollover is required, namely
79
+         * the residue could potentially be split between the end of buffer and the beginning and would
80
+         * require some complicated logic to handle. Using the lcm as the size of the buffer will
81
+         * guarantee that by the time the buffer reaches the end the residue will be a multiple of the
82
+         * `procNodeSampleRate` and the residue won't be split.
83
+         */
84
+        this._circularBufferLength = leastCommonMultiple(this._procNodeSampleRate, this._denoiseSampleSize);
85
+        this._circularBuffer = new Float32Array(this._circularBufferLength);
86
+    }
87
+
88
+    /**
89
+     * Worklet interface process method. The inputs parameter contains PCM audio that is then sent to rnnoise.
90
+     * Rnnoise only accepts PCM samples of 480 bytes whereas `process` handles 128 sized samples, we take this into
91
+     * account using a circular buffer.
92
+     *
93
+     * @param {Float32Array[]} inputs - Array of inputs connected to the node, each of them with their associated
94
+     * array of channels. Each channel is an array of 128 pcm samples.
95
+     * @param {Float32Array[]} outputs - Array of outputs similar to the inputs parameter structure, expected to be
96
+     * filled during the execution of `process`. By default each channel is zero filled.
97
+     * @returns {boolean} - Boolean value that returns whether or not the processor should remain active. Returning
98
+     * false will terminate it.
99
+     */
100
+    process(inputs: Float32Array[][], outputs: Float32Array[][]) {
101
+
102
+        // We expect the incoming track to be mono, if a stereo track is passed only on of its channels will get
103
+        // denoised and sent pack.
104
+        // TODO Technically we can denoise both channel however this might require a new rnnoise context, some more
105
+        // investigation is required.
106
+        const inData = inputs[0][0];
107
+        const outData = outputs[0][0];
108
+
109
+        // Append new raw PCM sample.
110
+        this._circularBuffer.set(inData, this._inputBufferLength);
111
+        this._inputBufferLength += inData.length;
112
+
113
+        // New raw samples were just added, start denoising frames, _denoisedBufferLength gives us
114
+        // the position at which the previous denoise iteration ended, basically it takes into account
115
+        // residue data.
116
+        for (; this._denoisedBufferLength + this._denoiseSampleSize <= this._inputBufferLength;
117
+            this._denoisedBufferLength += this._denoiseSampleSize) {
118
+            // Create view of circular buffer so it can be modified in place, removing the need for
119
+            // extra copies.
120
+
121
+            const denoiseFrame = this._circularBuffer.subarray(
122
+                this._denoisedBufferLength,
123
+                this._denoisedBufferLength + this._denoiseSampleSize
124
+            );
125
+
126
+            this._denoiseProcessor.processAudioFrame(denoiseFrame, true);
127
+        }
128
+
129
+        // Determine how much denoised audio is available, if the start index of denoised samples is smaller
130
+        // then _denoisedBufferLength that means a rollover occured.
131
+        let unsentDenoisedDataLength;
132
+
133
+        if (this._denoisedBufferIndx > this._denoisedBufferLength) {
134
+            unsentDenoisedDataLength = this._circularBufferLength - this._denoisedBufferIndx;
135
+        } else {
136
+            unsentDenoisedDataLength = this._denoisedBufferLength - this._denoisedBufferIndx;
137
+        }
138
+
139
+        // Only copy denoised data to output when there's enough of it to fit the exact buffer length.
140
+        // e.g. if the buffer size is 1024 samples but we only denoised 960 (this happens on the first iteration)
141
+        // nothing happens, then on the next iteration 1920 samples will be denoised so we send 1024 which leaves
142
+        // 896 for the next iteration and so on.
143
+        if (unsentDenoisedDataLength >= outData.length) {
144
+            const denoisedFrame = this._circularBuffer.subarray(
145
+                this._denoisedBufferIndx,
146
+                this._denoisedBufferIndx + outData.length
147
+            );
148
+
149
+            outData.set(denoisedFrame, 0);
150
+            this._denoisedBufferIndx += outData.length;
151
+        }
152
+
153
+        // When the end of the circular buffer has been reached, start from the beggining. By the time the index
154
+        // starts over, the data from the begging is stale (has already been processed) and can be safely
155
+        // overwritten.
156
+        if (this._denoisedBufferIndx === this._circularBufferLength) {
157
+            this._denoisedBufferIndx = 0;
158
+        }
159
+
160
+        // Because the circular buffer's length is the lcm of both input size and the processor's sample size,
161
+        // by the time we reach the end with the input index the denoise length index will be there as well.
162
+        if (this._inputBufferLength === this._circularBufferLength) {
163
+            this._inputBufferLength = 0;
164
+            this._denoisedBufferLength = 0;
165
+        }
166
+
167
+        return true;
168
+    }
169
+}
170
+
171
+registerProcessor('NoiseSuppressorWorklet', NoiseSuppressorWorklet);

+ 4
- 0
react/features/stream-effects/noise-suppression/logger.ts View File

@@ -0,0 +1,4 @@
1
+// @ts-ignore
2
+import { getLogger } from '../../base/logging/functions';
3
+
4
+export default getLogger('features/stream-effects/noise-suppression');

react/features/stream-effects/rnnoise/RnnoiseProcessor.js → react/features/stream-effects/rnnoise/RnnoiseProcessor.ts View File

@@ -1,9 +1,15 @@
1
-// @flow
1
+/* eslint-disable no-bitwise */
2
+
3
+interface RnnoiseModule extends EmscriptenModule {
4
+    _rnnoise_create() : number;
5
+    _rnnoise_destroy(context: number): void;
6
+    _rnnoise_process_frame(context: number, input: number, output: number): number;
7
+}
2 8
 
3 9
 /**
4 10
  * Constant. Rnnoise default sample size, samples of different size won't work.
5 11
  */
6
-export const RNNOISE_SAMPLE_LENGTH: number = 480;
12
+export const RNNOISE_SAMPLE_LENGTH = 480;
7 13
 
8 14
 /**
9 15
  *  Constant. Rnnoise only takes inputs of 480 PCM float32 samples thus 480*4.
@@ -13,7 +19,12 @@ const RNNOISE_BUFFER_SIZE: number = RNNOISE_SAMPLE_LENGTH * 4;
13 19
 /**
14 20
  *  Constant. Rnnoise only takes operates on 44.1Khz float 32 little endian PCM.
15 21
  */
16
-const PCM_FREQUENCY: number = 44100;
22
+const PCM_FREQUENCY = 44100;
23
+
24
+/**
25
+ * Used to shift a 32 bit number by 16 bits.
26
+ */
27
+const SHIFT_16_BIT_NR = 32768;
17 28
 
18 29
 /**
19 30
  * Represents an adaptor for the rnnoise library compiled to webassembly. The class takes care of webassembly
@@ -24,32 +35,27 @@ export default class RnnoiseProcessor {
24 35
     /**
25 36
      * Rnnoise context object needed to perform the audio processing.
26 37
      */
27
-    _context: ?Object;
38
+    private _context: number;
28 39
 
29 40
     /**
30 41
      * State flag, check if the instance was destroyed.
31 42
      */
32
-    _destroyed: boolean = false;
43
+    private _destroyed = false;
33 44
 
34 45
     /**
35 46
      * WASM interface through which calls to rnnoise are made.
36 47
      */
37
-    _wasmInterface: Object;
48
+    private _wasmInterface: RnnoiseModule;
38 49
 
39 50
     /**
40 51
      * WASM dynamic memory buffer used as input for rnnoise processing method.
41 52
      */
42
-    _wasmPcmInput: Object;
53
+    private _wasmPcmInput: number;
43 54
 
44 55
     /**
45 56
      * The Float32Array index representing the start point in the wasm heap of the _wasmPcmInput buffer.
46 57
      */
47
-    _wasmPcmInputF32Index: number;
48
-
49
-    /**
50
-     * WASM dynamic memory buffer used as output for rnnoise processing method.
51
-     */
52
-    _wasmPcmOutput: Object;
58
+    private _wasmPcmInputF32Index: number;
53 59
 
54 60
     /**
55 61
      * Constructor.
@@ -57,7 +63,7 @@ export default class RnnoiseProcessor {
57 63
      * @class
58 64
      * @param {Object} wasmInterface - WebAssembly module interface that exposes rnnoise functionality.
59 65
      */
60
-    constructor(wasmInterface: Object) {
66
+    constructor(wasmInterface: RnnoiseModule) {
61 67
         // Considering that we deal with dynamic allocated memory employ exception safety strong guarantee
62 68
         // i.e. in case of exception there are no side effects.
63 69
         try {
@@ -66,73 +72,34 @@ export default class RnnoiseProcessor {
66 72
             // For VAD score purposes only allocate the buffers once and reuse them
67 73
             this._wasmPcmInput = this._wasmInterface._malloc(RNNOISE_BUFFER_SIZE);
68 74
 
75
+            this._wasmPcmInputF32Index = this._wasmPcmInput >> 2;
76
+
69 77
             if (!this._wasmPcmInput) {
70 78
                 throw Error('Failed to create wasm input memory buffer!');
71 79
             }
72 80
 
73
-            this._wasmPcmOutput = this._wasmInterface._malloc(RNNOISE_BUFFER_SIZE);
74
-
75
-            if (!this._wasmPcmOutput) {
76
-                wasmInterface._free(this._wasmPcmInput);
77
-                throw Error('Failed to create wasm output memory buffer!');
78
-            }
79
-
80
-            // The HEAPF32.set function requires an index relative to a Float32 array view of the wasm memory model
81
-            // which is an array of bytes. This means we have to divide it by the size of a float to get the index
82
-            // relative to a Float32 Array.
83
-            this._wasmPcmInputF32Index = this._wasmPcmInput / 4;
84
-
85 81
             this._context = this._wasmInterface._rnnoise_create();
86 82
         } catch (error) {
87 83
             // release can be called even if not all the components were initialized.
88
-            this._releaseWasmResources();
84
+            this.destroy();
89 85
             throw error;
90 86
         }
91 87
     }
92 88
 
93
-    /**
94
-     * Copy the input PCM Audio Sample to the wasm input buffer.
95
-     *
96
-     * @param {Float32Array} pcmSample - Array containing 16 bit format PCM sample stored in 32 Floats .
97
-     * @returns {void}
98
-     */
99
-    _copyPCMSampleToWasmBuffer(pcmSample: Float32Array) {
100
-        this._wasmInterface.HEAPF32.set(pcmSample, this._wasmPcmInputF32Index);
101
-    }
102
-
103
-    /**
104
-     * Convert 32 bit Float PCM samples to 16 bit Float PCM samples and store them in 32 bit Floats.
105
-     *
106
-     * @param {Float32Array} f32Array - Array containing 32 bit PCM samples.
107
-     * @returns {void}
108
-     */
109
-    _convertTo16BitPCM(f32Array: Float32Array) {
110
-        for (const [ index, value ] of f32Array.entries()) {
111
-            f32Array[index] = value * 0x7fff;
112
-        }
113
-    }
114
-
115 89
     /**
116 90
      * Release resources associated with the wasm context. If something goes downhill here
117 91
      * i.e. Exception is thrown, there is nothing much we can do.
118 92
      *
119 93
      * @returns {void}
120 94
      */
121
-    _releaseWasmResources() {
95
+    _releaseWasmResources(): void {
122 96
         // For VAD score purposes only allocate the buffers once and reuse them
123 97
         if (this._wasmPcmInput) {
124 98
             this._wasmInterface._free(this._wasmPcmInput);
125
-            this._wasmPcmInput = null;
126
-        }
127
-
128
-        if (this._wasmPcmOutput) {
129
-            this._wasmInterface._free(this._wasmPcmOutput);
130
-            this._wasmPcmOutput = null;
131 99
         }
132 100
 
133 101
         if (this._context) {
134 102
             this._wasmInterface._rnnoise_destroy(this._context);
135
-            this._context = null;
136 103
         }
137 104
     }
138 105
 
@@ -141,7 +108,7 @@ export default class RnnoiseProcessor {
141 108
      *
142 109
      * @returns {number} - The PCM sample array size as required by rnnoise.
143 110
      */
144
-    getSampleLength() {
111
+    getSampleLength(): number {
145 112
         return RNNOISE_SAMPLE_LENGTH;
146 113
     }
147 114
 
@@ -150,7 +117,7 @@ export default class RnnoiseProcessor {
150 117
      *
151 118
      * @returns {number} - PCM sample frequency as required by rnnoise.
152 119
      */
153
-    getRequiredPCMFrequency() {
120
+    getRequiredPCMFrequency(): number {
154 121
         return PCM_FREQUENCY;
155 122
     }
156 123
 
@@ -160,7 +127,7 @@ export default class RnnoiseProcessor {
160 127
      *
161 128
      * @returns {void}
162 129
      */
163
-    destroy() {
130
+    destroy(): void {
164 131
         // Attempting to release a non initialized processor, do nothing.
165 132
         if (this._destroyed) {
166 133
             return;
@@ -176,22 +143,44 @@ export default class RnnoiseProcessor {
176 143
      * The size of the array must be of exactly 480 samples, this constraint comes from the rnnoise library.
177 144
      *
178 145
      * @param {Float32Array} pcmFrame - Array containing 32 bit PCM samples.
146
+     * @returns {Float} Contains VAD score in the interval 0 - 1 i.e. 0.90.
147
+     */
148
+    calculateAudioFrameVAD(pcmFrame: Float32Array): number {
149
+        return this.processAudioFrame(pcmFrame);
150
+    }
151
+
152
+    /**
153
+     * Process an audio frame, optionally denoising the input pcmFrame and returning the Voice Activity Detection score
154
+     * for a raw Float32 PCM sample Array.
155
+     * The size of the array must be of exactly 480 samples, this constraint comes from the rnnoise library.
156
+     *
157
+     * @param {Float32Array} pcmFrame - Array containing 32 bit PCM samples. Parameter is also used as output
158
+     * when {@code shouldDenoise} is true.
159
+     * @param {boolean} shouldDenoise - Should the denoised frame be returned in pcmFrame.
179 160
      * @returns {Float} Contains VAD score in the interval 0 - 1 i.e. 0.90 .
180 161
      */
181
-    calculateAudioFrameVAD(pcmFrame: Float32Array) {
182
-        if (this._destroyed) {
183
-            throw new Error('RnnoiseProcessor instance is destroyed, please create another one!');
162
+    processAudioFrame(pcmFrame: Float32Array, shouldDenoise: Boolean = false): number {
163
+        // Convert 32 bit Float PCM samples to 16 bit Float PCM samples as that's what rnnoise accepts as input
164
+        for (let i = 0; i < RNNOISE_SAMPLE_LENGTH; i++) {
165
+            this._wasmInterface.HEAPF32[this._wasmPcmInputF32Index + i] = pcmFrame[i] * SHIFT_16_BIT_NR;
184 166
         }
185 167
 
186
-        const pcmFrameLength = pcmFrame.length;
187
-
188
-        if (pcmFrameLength !== RNNOISE_SAMPLE_LENGTH) {
189
-            throw new Error(`Rnnoise can only process PCM frames of 480 samples! Input sample was:${pcmFrameLength}`);
168
+        // Use the same buffer for input/output, rnnoise supports this behavior
169
+        const vadScore = this._wasmInterface._rnnoise_process_frame(
170
+            this._context,
171
+            this._wasmPcmInput,
172
+            this._wasmPcmInput
173
+        );
174
+
175
+        // Rnnoise denoises the frame by default but we can avoid unnecessary operations if the calling
176
+        // client doesn't use the denoised frame.
177
+        if (shouldDenoise) {
178
+            // Convert back to 32 bit PCM
179
+            for (let i = 0; i < RNNOISE_SAMPLE_LENGTH; i++) {
180
+                pcmFrame[i] = this._wasmInterface.HEAPF32[this._wasmPcmInputF32Index + i] / SHIFT_16_BIT_NR;
181
+            }
190 182
         }
191 183
 
192
-        this._convertTo16BitPCM(pcmFrame);
193
-        this._copyPCMSampleToWasmBuffer(pcmFrame);
194
-
195
-        return this._wasmInterface._rnnoise_process_frame(this._context, this._wasmPcmOutput, this._wasmPcmInput);
184
+        return vadScore;
196 185
     }
197 186
 }

+ 2
- 2
react/features/stream-effects/rnnoise/index.js View File

@@ -2,7 +2,7 @@
2 2
 
3 3
 // Script expects to find rnnoise webassembly binary in the same public path root, otherwise it won't load
4 4
 // During the build phase this needs to be taken care of manually
5
-import rnnoiseWasmInit from 'rnnoise-wasm';
5
+import { createRNNWasmModule } from '@jitsi/rnnoise-wasm';
6 6
 
7 7
 import RnnoiseProcessor from './RnnoiseProcessor';
8 8
 
@@ -18,7 +18,7 @@ let rnnoiseModule;
18 18
  */
19 19
 export function createRnnoiseProcessor() {
20 20
     if (!rnnoiseModule) {
21
-        rnnoiseModule = rnnoiseWasmInit();
21
+        rnnoiseModule = createRNNWasmModule();
22 22
     }
23 23
 
24 24
     return rnnoiseModule.then(mod => new RnnoiseProcessor(mod));

+ 9
- 0
react/features/toolbox/components/web/Toolbox.js View File

@@ -36,6 +36,7 @@ import { isGifEnabled } from '../../../gifs/functions';
36 36
 import { InviteButton } from '../../../invite/components/add-people-dialog';
37 37
 import { isVpaasMeeting } from '../../../jaas/functions';
38 38
 import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts';
39
+import { NoiseSuppressionButton } from '../../../noise-suppression/components';
39 40
 import {
40 41
     close as closeParticipantsPane,
41 42
     open as openParticipantsPane
@@ -761,6 +762,13 @@ class Toolbox extends Component<Props> {
761 762
             group: 3
762 763
         };
763 764
 
765
+        const noiseSuppression = {
766
+            key: 'noisesuppression',
767
+            Content: NoiseSuppressionButton,
768
+            group: 3
769
+        };
770
+
771
+
764 772
         const etherpad = {
765 773
             key: 'etherpad',
766 774
             Content: SharedDocumentButton,
@@ -847,6 +855,7 @@ class Toolbox extends Component<Props> {
847 855
             linkToSalesforce,
848 856
             shareVideo,
849 857
             shareAudio,
858
+            noiseSuppression,
850 859
             etherpad,
851 860
             virtualBackground,
852 861
             dockIframe,

+ 2
- 1
tsconfig.json View File

@@ -9,7 +9,8 @@
9 9
         "noEmit": false,
10 10
         "moduleResolution": "Node",
11 11
         "strict": true,
12
-        "noImplicitAny": true
12
+        "noImplicitAny": true,
13
+        "strictPropertyInitialization": false
13 14
     },
14 15
     "exclude": [
15 16
         "node_modules"

+ 34
- 1
webpack.config.js View File

@@ -2,7 +2,7 @@
2 2
 
3 3
 const CircularDependencyPlugin = require('circular-dependency-plugin');
4 4
 const fs = require('fs');
5
-const { join } = require('path');
5
+const { join, resolve } = require('path');
6 6
 const process = require('process');
7 7
 const webpack = require('webpack');
8 8
 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
@@ -392,6 +392,39 @@ module.exports = (_env, argv) => {
392 392
                 ...getBundleAnalyzerPlugin(analyzeBundle, 'face-landmarks-worker')
393 393
             ],
394 394
             performance: getPerformanceHints(perfHintOptions, 1024 * 1024 * 2)
395
+        }),
396
+        Object.assign({}, config, {
397
+            /**
398
+             * The NoiseSuppressorWorklet is loaded in an audio worklet which doesn't have the same
399
+             * context as a normal window, (e.g. self/window is not defined).
400
+             * While running a production build webpack's boilerplate code doesn't introduce any
401
+             * audio worklet "unfriendly" code however when running the dev server, hot module replacement
402
+             * and live reload add javascript code that can't be ran by the worklet, so we explicity ignore
403
+             * those parts with the null-loader.
404
+             * The dev server also expects a `self` global object that's not available in the `AudioWorkletGlobalScope`,
405
+             * so we replace it.
406
+             */
407
+            entry: {
408
+                'noise-suppressor-worklet':
409
+                    './react/features/stream-effects/noise-suppression/NoiseSuppressorWorklet.ts'
410
+            },
411
+
412
+            module: { rules: [
413
+                ...config.module.rules,
414
+                {
415
+                    test: resolve(__dirname, 'node_modules/webpack-dev-server/client'),
416
+                    loader: 'null-loader'
417
+                }
418
+            ] },
419
+            plugins: [
420
+            ],
421
+            performance: getPerformanceHints(perfHintOptions, 200 * 1024),
422
+
423
+            output: {
424
+                ...config.output,
425
+
426
+                globalObject: 'AudioWorkletGlobalScope'
427
+            }
395 428
         })
396 429
     ];
397 430
 };

Loading…
Cancel
Save