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"