(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:
falkTX
2026-02-23 21:02:56 +01:00
committed by aleb_the_flash
parent 64819b3696
commit 13036f6ab7
6 changed files with 31 additions and 23 deletions
+4
View File
@@ -8,6 +8,10 @@ and this project adheres to
## [Unreleased]
### Changed
- ✨(frontend) enhance noise reduction with BBBA audio processing pipeline
## [1.20.0] - 2026-06-12
### Changed
+7
View File
@@ -11,6 +11,7 @@
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4",
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2",
@@ -1446,6 +1447,12 @@
"@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": {
"version": "0.12.13",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
+1
View File
@@ -17,6 +17,7 @@
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
"@libreaudio/la-call": "0.1.4",
"@fontsource/opendyslexic": "5.2.5",
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
@@ -216,6 +216,14 @@ export const Join = ({
try {
const track = await createLocalAudioTrack({
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)
} 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
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
import type { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
import { createWasmProcessor } from '@libreaudio/la-call'
// Use Jitsi's approach: maintain a global AudioContext variable
// and suspend/resume it as needed to manage audio state
@@ -20,7 +17,7 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
private source?: MediaStreamTrack
private sourceNode?: MediaStreamAudioSourceNode
private destinationNode?: MediaStreamAudioDestinationNode
private noiseSuppressionNode?: AudioWorkletNode
private noiseSuppressionNode?: AudioNode
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
if (!opts.track) {
@@ -35,25 +32,16 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
await audioContext.resume()
}
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
this.sourceNode = audioContext.createMediaStreamSource(
new MediaStream([this.source])
)
this.noiseSuppressionNode = new AudioWorkletNode(
audioContext,
NoiseSuppressorWorklet_Name,
{
// RNNoise is a mono algorithm. Its worklet only denoises and writes
// 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.noiseSuppressionNode = await createWasmProcessor(audioContext, {
intensity: 90,
})
if (!this.noiseSuppressionNode) {
throw new Error('Failed to create Wasm processor')
}
this.destinationNode = audioContext.createMediaStreamDestination()
+2 -2
View File
@@ -21,8 +21,8 @@
"disabled": "Microphone disabled"
},
"noiseReduction": {
"label": "Noise reduction",
"heading": "Noise reduction",
"label": "Advanced voice optimization",
"heading": "Audio filters",
"ariaLabel": {
"enable": "Enable noise reduction",
"disable": "Disable noise reduction"