From 13036f6ab71540afe45496d6df22896b5ebb4167 Mon Sep 17 00:00:00 2001 From: falkTX Date: Mon, 23 Feb 2026 21:02:56 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20enhance=20noise=20reducti?= =?UTF-8?q?on=20with=20BBBA=20audio=20processing=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 4 +++ src/frontend/package-lock.json | 7 +++++ src/frontend/package.json | 1 + .../src/features/rooms/components/Join.tsx | 8 +++++ .../livekit/processors/RnnNoiseProcessor.ts | 30 ++++++------------- src/frontend/src/locales/en/settings.json | 4 +-- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a56c13..5e84cd7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 3e0cbbd1..41c67d20 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -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", diff --git a/src/frontend/package.json b/src/frontend/package.json index 9dbe2ee1..23c535e3 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -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", diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 7568a11c..b078cab6 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -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) { diff --git a/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts b/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts index d8324bdb..195ab439 100644 --- a/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts +++ b/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts @@ -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) { 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() diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json index 2214d222..ff79348d 100644 --- a/src/frontend/src/locales/en/settings.json +++ b/src/frontend/src/locales/en/settings.json @@ -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"