mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 18:56:58 +00:00
✨(frontend) enhance noise reduction with BBBA audio processing pipeline
Replace the basic RNN noise processor with a more advanced audio pipeline powered by Big Blue Better Audio (BBBA), a project supported by the Prototype Fund. The new WASM-based pipeline introduced by @falkTX and @trummerschlunk adds: - voice isolation - high-pass filtering - spectral balancing - multiband compression - low-pass filtering This significantly improves overall audio quality and speech clarity. More information about the pipeline is available on the trummerschlunk/BigBlueBetterAudio repo. Voice isolation still relies on RNNNoise, a widely used deep learning-based denoising model. Audio quality could be further improved in the future if DeepFilterNet becomes usable directly in the browser. Their code has been released in an NPM package under GPL license.
This commit is contained in:
@@ -8,6 +8,10 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ✨(frontend) enhance noise reduction with BBBA audio processing pipeline
|
||||||
|
|
||||||
## [1.20.0] - 2026-06-12
|
## [1.20.0] - 2026-06-12
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+7
@@ -11,6 +11,7 @@
|
|||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||||
"@fontsource-variable/lexend": "5.2.11",
|
"@fontsource-variable/lexend": "5.2.11",
|
||||||
"@fontsource/opendyslexic": "5.2.5",
|
"@fontsource/opendyslexic": "5.2.5",
|
||||||
|
"@libreaudio/la-call": "0.1.4",
|
||||||
"@livekit/components-react": "2.9.21",
|
"@livekit/components-react": "2.9.21",
|
||||||
"@livekit/components-styles": "1.2.0",
|
"@livekit/components-styles": "1.2.0",
|
||||||
"@livekit/track-processors": "0.7.2",
|
"@livekit/track-processors": "0.7.2",
|
||||||
@@ -1446,6 +1447,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@libreaudio/la-call": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@libreaudio/la-call/-/la-call-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-yaqlYGpvZbfpr1h2p1jSGOKPUy7NsfpoJN9gXvHmJu5MB8nN/qfx6LQ2a0hbua3H8rargFKROOv4VZ5WuTKNsA==",
|
||||||
|
"license": "GPL-3.0+"
|
||||||
|
},
|
||||||
"node_modules/@livekit/components-core": {
|
"node_modules/@livekit/components-core": {
|
||||||
"version": "0.12.13",
|
"version": "0.12.13",
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
|
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||||
"@fontsource-variable/lexend": "5.2.11",
|
"@fontsource-variable/lexend": "5.2.11",
|
||||||
|
"@libreaudio/la-call": "0.1.4",
|
||||||
"@fontsource/opendyslexic": "5.2.5",
|
"@fontsource/opendyslexic": "5.2.5",
|
||||||
"@livekit/components-react": "2.9.21",
|
"@livekit/components-react": "2.9.21",
|
||||||
"@livekit/components-styles": "1.2.0",
|
"@livekit/components-styles": "1.2.0",
|
||||||
|
|||||||
@@ -216,6 +216,14 @@ export const Join = ({
|
|||||||
try {
|
try {
|
||||||
const track = await createLocalAudioTrack({
|
const track = await createLocalAudioTrack({
|
||||||
deviceId: { exact: audioDeviceId },
|
deviceId: { exact: audioDeviceId },
|
||||||
|
noiseSuppression: true,
|
||||||
|
echoCancellation: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
voiceIsolation: false,
|
||||||
|
// Audio quality optimized for voice
|
||||||
|
sampleRate: 48000, // High quality sample rate
|
||||||
|
channelCount: 1, // Mono for voice calls (saves bandwidth)
|
||||||
|
sampleSize: 16, // 16-bit audio
|
||||||
})
|
})
|
||||||
setDynamicAudioTrack(track)
|
setDynamicAudioTrack(track)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
|
|
||||||
import { NoiseSuppressorWorklet_Name } from '@timephy/rnnoise-wasm'
|
|
||||||
|
|
||||||
// This is an example how to get the script path using Vite, may be different when using other build tools
|
import type { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
|
||||||
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
|
import { createWasmProcessor } from '@libreaudio/la-call'
|
||||||
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
|
|
||||||
|
|
||||||
// Use Jitsi's approach: maintain a global AudioContext variable
|
// Use Jitsi's approach: maintain a global AudioContext variable
|
||||||
// and suspend/resume it as needed to manage audio state
|
// and suspend/resume it as needed to manage audio state
|
||||||
@@ -20,7 +17,7 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
|
|||||||
private source?: MediaStreamTrack
|
private source?: MediaStreamTrack
|
||||||
private sourceNode?: MediaStreamAudioSourceNode
|
private sourceNode?: MediaStreamAudioSourceNode
|
||||||
private destinationNode?: MediaStreamAudioDestinationNode
|
private destinationNode?: MediaStreamAudioDestinationNode
|
||||||
private noiseSuppressionNode?: AudioWorkletNode
|
private noiseSuppressionNode?: AudioNode
|
||||||
|
|
||||||
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
|
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
|
||||||
if (!opts.track) {
|
if (!opts.track) {
|
||||||
@@ -35,25 +32,16 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
|
|||||||
await audioContext.resume()
|
await audioContext.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
|
|
||||||
|
|
||||||
this.sourceNode = audioContext.createMediaStreamSource(
|
this.sourceNode = audioContext.createMediaStreamSource(
|
||||||
new MediaStream([this.source])
|
new MediaStream([this.source])
|
||||||
)
|
)
|
||||||
|
|
||||||
this.noiseSuppressionNode = new AudioWorkletNode(
|
this.noiseSuppressionNode = await createWasmProcessor(audioContext, {
|
||||||
audioContext,
|
intensity: 90,
|
||||||
NoiseSuppressorWorklet_Name,
|
})
|
||||||
{
|
if (!this.noiseSuppressionNode) {
|
||||||
// RNNoise is a mono algorithm. Its worklet only denoises and writes
|
throw new Error('Failed to create Wasm processor')
|
||||||
// channel 0. Force any (possibly stereo) input to down-mix to a single
|
}
|
||||||
// channel and emit a single channel, so we don't produce a stereo frame
|
|
||||||
// whose right channel is left silent.
|
|
||||||
channelCount: 1,
|
|
||||||
channelCountMode: 'explicit',
|
|
||||||
outputChannelCount: [1],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
this.destinationNode = audioContext.createMediaStreamDestination()
|
this.destinationNode = audioContext.createMediaStreamDestination()
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
"disabled": "Microphone disabled"
|
"disabled": "Microphone disabled"
|
||||||
},
|
},
|
||||||
"noiseReduction": {
|
"noiseReduction": {
|
||||||
"label": "Noise reduction",
|
"label": "Advanced voice optimization",
|
||||||
"heading": "Noise reduction",
|
"heading": "Audio filters",
|
||||||
"ariaLabel": {
|
"ariaLabel": {
|
||||||
"enable": "Enable noise reduction",
|
"enable": "Enable noise reduction",
|
||||||
"disable": "Disable noise reduction"
|
"disable": "Disable noise reduction"
|
||||||
|
|||||||
Reference in New Issue
Block a user