Skip to content

Commit 889fb8b

Browse files
committed
feat: implement NoiseSuppressor node for audio processing
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 934ce62 commit 889fb8b

5 files changed

Lines changed: 138 additions & 3 deletions

File tree

src/components/MediaSettings/AdvancedAudioDialog.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import NcFormBox from '@nextcloud/vue/components/NcFormBox'
1010
import NcFormBoxSwitch from '@nextcloud/vue/components/NcFormBoxSwitch'
1111
import { useDevices } from '../../composables/useDevices.js'
1212
import { useSettingsStore } from '../../stores/settings.ts'
13+
import { localMediaModel } from '../../utils/webrtc/index.js'
1314
1415
const props = defineProps<{
1516
container?: string
@@ -66,6 +67,14 @@ function onClosing(result?: unknown) {
6667
)) {
6768
// Apply changes to audio stream
6869
updateAudioStream(true)
70+
71+
if (localMediaModel.getWebRtc()) {
72+
if (settingsStore.noiseSuppressionWithModel) {
73+
localMediaModel.enableNoiseSuppression()
74+
} else {
75+
localMediaModel.disableNoiseSuppression()
76+
}
77+
}
6978
}
7079
7180
emit('close', result)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import {
7+
destroyNoiseSuppressionWorklet,
8+
processNoiseSuppression,
9+
registerNoiseSuppressionWorklet,
10+
} from '../../suppressNoise.ts'
11+
import TrackSinkSource from './TrackSinkSource.js'
12+
13+
/**
14+
* Processor node to enable or disable noise suppression.
15+
*
16+
* A single input track slot with the default id is accepted. A single output
17+
* track slot with the default id is provided.
18+
*
19+
* The track can be enabled and disabled by calling "setEnabled(bool)".
20+
*
21+
* When enabled, the node processes the input audio track to suppress noise and
22+
* outputs a new, processed track. When disabled, it passes the original audio
23+
* track through without modification.
24+
*
25+
* The node manages the lifecycle of the underlying noise suppression worklet,
26+
* registering a consumer when enabled and destroying it when disabled.
27+
*
28+
* -----------------
29+
* | |
30+
* ---> | NoiseSuppressor | --->
31+
* | |
32+
* -----------------
33+
*/
34+
export default class NoiseSuppressor extends TrackSinkSource {
35+
constructor() {
36+
super()
37+
38+
this._addInputTrackSlot()
39+
this._addOutputTrackSlot()
40+
41+
this._enabled = false
42+
this._audioEnabled = true
43+
this.noiseSuppressionConsumer = null
44+
}
45+
46+
isEnabled() {
47+
return this._enabled
48+
}
49+
50+
async setEnabled(enabled) {
51+
if (this._enabled === enabled) {
52+
return
53+
}
54+
55+
this._enabled = enabled
56+
57+
if (enabled) {
58+
this.noiseSuppressionConsumer = await registerNoiseSuppressionWorklet()
59+
} else if (this.noiseSuppressionConsumer) {
60+
await destroyNoiseSuppressionWorklet(this.noiseSuppressionConsumer)
61+
this.noiseSuppressionConsumer = null
62+
}
63+
64+
const track = this.getInputTrack('default')
65+
this._handleInputTrack('default', track)
66+
}
67+
68+
_handleInputTrack(trackId, track) {
69+
if (track) {
70+
this._audioEnabled = track.enabled
71+
}
72+
if (!this._enabled || !track) {
73+
this._setOutputTrack('default', track)
74+
return
75+
}
76+
77+
const inputStream = new MediaStream([track])
78+
const processedStream = processNoiseSuppression(inputStream, true)
79+
const processedTrack = processedStream.getAudioTracks()[0]
80+
processedTrack.enabled = this._audioEnabled
81+
82+
this._setOutputTrack('default', processedTrack)
83+
}
84+
85+
_handleInputTrackEnabled(trackId, enabled) {
86+
if (enabled !== this._audioEnabled) {
87+
this._audioEnabled = enabled
88+
this._setOutputTrackEnabled('default', enabled)
89+
}
90+
}
91+
}

src/utils/webrtc/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ async function signalingJoinCall(token, flags, silent, recordingConsent, silentF
262262

263263
// The previous state might be wiped after the media is started, so
264264
// it should be saved now.
265+
const noiseSuppressionWithModel = BrowserStorage.getItem('noiseSuppressionWithModel') === 'true'
265266
const enableAudio = !BrowserStorage.getItem('audioDisabled_' + token)
266267
const enableVideo = !BrowserStorage.getItem('videoDisabled_' + token)
267268
const enableVirtualBackground = !!BrowserStorage.getItem('virtualBackgroundEnabled_' + token)
@@ -286,6 +287,11 @@ async function signalingJoinCall(token, flags, silent, recordingConsent, silentF
286287
} else {
287288
localMediaModel.disableVirtualBackground()
288289
}
290+
if (noiseSuppressionWithModel) {
291+
localMediaModel.enableNoiseSuppression()
292+
} else {
293+
localMediaModel.disableNoiseSuppression()
294+
}
289295
if (virtualBackgroundType === VIRTUAL_BACKGROUND.BACKGROUND_TYPE.IMAGE) {
290296
localMediaModel.setVirtualBackgroundImage(virtualBackgroundUrl)
291297
} else if (virtualBackgroundType === VIRTUAL_BACKGROUND.BACKGROUND_TYPE.VIDEO) {

src/utils/webrtc/models/LocalMediaModel.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,20 @@ LocalMediaModel.prototype = {
377377
this._webRtc.pauseVideo()
378378
},
379379

380+
enableNoiseSuppression() {
381+
if (!this._webRtc) {
382+
throw new Error('WebRtc not initialized yet')
383+
}
384+
this._webRtc.webrtc.enableNoiseSuppression()
385+
},
386+
387+
disableNoiseSuppression() {
388+
if (!this._webRtc) {
389+
throw new Error('WebRtc not initialized yet')
390+
}
391+
this._webRtc.webrtc.disableNoiseSuppression()
392+
},
393+
380394
enableVirtualBackground() {
381395
if (!this._webRtc) {
382396
throw new Error('WebRtc not initialized yet')

src/utils/webrtc/simplewebrtc/localmedia.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import util from 'util'
88
import WildEmitter from 'wildemitter'
99
import BlackVideoEnforcer from '../../media/pipeline/BlackVideoEnforcer.js'
1010
import MediaDevicesSource from '../../media/pipeline/MediaDevicesSource.js'
11+
import NoiseSuppressor from '../../media/pipeline/NoiseSuppressor.js'
1112
import SpeakingMonitor from '../../media/pipeline/SpeakingMonitor.js'
1213
import TrackConstrainer from '../../media/pipeline/TrackConstrainer.js'
1314
import TrackEnabler from '../../media/pipeline/TrackEnabler.js'
@@ -55,6 +56,8 @@ export default function LocalMedia(opts) {
5556
this._audioTrackEnabler = new TrackEnabler()
5657
this._videoTrackEnabler = new TrackEnabler()
5758

59+
this._noiseSuppressor = new NoiseSuppressor()
60+
5861
this._videoTrackConstrainer = new TrackConstrainer()
5962

6063
this._virtualBackground = new VirtualBackground()
@@ -99,9 +102,11 @@ export default function LocalMedia(opts) {
99102
this._mediaDevicesSource.connectTrackSink('audio', this._audioTrackEnabler)
100103
this._mediaDevicesSource.connectTrackSink('video', this._videoTrackEnabler)
101104

102-
this._audioTrackEnabler.connectTrackSink('default', this._speakingMonitor)
103-
this._audioTrackEnabler.connectTrackSink('default', this._trackToStream, 'audio')
104-
this._audioTrackEnabler.connectTrackSink('default', this._trackToSentStream, 'audio')
105+
this._audioTrackEnabler.connectTrackSink('default', this._noiseSuppressor)
106+
107+
this._noiseSuppressor.connectTrackSink('default', this._speakingMonitor)
108+
this._noiseSuppressor.connectTrackSink('default', this._trackToStream, 'audio')
109+
this._noiseSuppressor.connectTrackSink('default', this._trackToSentStream, 'audio')
105110

106111
this._videoTrackEnabler.connectTrackSink('default', this._videoTrackConstrainer)
107112

@@ -392,6 +397,16 @@ LocalMedia.prototype.resumeVideo = function() {
392397
this.emit('videoOn')
393398
}
394399

400+
LocalMedia.prototype.enableNoiseSuppression = function() {
401+
this._noiseSuppressor.setEnabled(true)
402+
this.emit('noiseSuppressionOn')
403+
}
404+
405+
LocalMedia.prototype.disableNoiseSuppression = function() {
406+
this._noiseSuppressor.setEnabled(false)
407+
this.emit('noiseSuppressionOff')
408+
}
409+
395410
LocalMedia.prototype.enableVirtualBackground = function() {
396411
this._virtualBackground.setEnabled(true)
397412
this.emit('virtualBackgroundOn')

0 commit comments

Comments
 (0)