feat: RNNoise WASM AudioWorklet for ML-based noise suppression (Phase 3)

Inject @sapphi-red/web-noise-suppressor into AudioManager's input pipeline
as a toggleable AudioWorkletNode. The worklet is loaded lazily on first
enable, then kept alive — toggling bypasses by rewiring the graph without
destroying the WASM instance. Browser NS is forced off when RNNoise is
active to avoid double-processing. InputGain forced to stereo up-mix to
prevent mono-left-only output from the worklet. Also wires Phase 2 output
device routing (setSinkId) into AppLayout/ChannelSidebar.
This commit is contained in:
Jannis Braun
2026-02-23 00:28:29 +01:00
parent dbebd38576
commit ccf2001047
15 changed files with 345 additions and 49 deletions
@@ -18,6 +18,8 @@ export function VoiceControls() {
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
const noiseSuppression = useVoiceStore((s) => s.noiseSuppression);
const toggleNoiseSuppression = useVoiceStore((s) => s.toggleNoiseSuppression);
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
const toggleRnnoise = useVoiceStore((s) => s.toggleRnnoise);
const connectionError = useVoiceStore((s) => s.connectionError);
const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected);
const channels = useServerStore((s) => s.channels);
@@ -172,7 +174,7 @@ export function VoiceControls() {
</svg>
</button>
{/* Noise Suppression */}
{/* Browser Noise Suppression */}
<button
onClick={handleNoiseSuppression}
className={`${btnBase} ${
@@ -180,7 +182,7 @@ export function VoiceControls() {
? 'bg-[#111214] text-discord-green hover:bg-[#1a1b1e]'
: btnDefaultStyle
}`}
title={noiseSuppression ? 'Disable Noise Suppression' : 'Enable Noise Suppression'}
title={noiseSuppression ? 'Disable Browser Noise Suppression' : 'Enable Browser Noise Suppression'}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 9v6h4l5 5V4l-5 5H7z" />
@@ -195,6 +197,34 @@ export function VoiceControls() {
</svg>
</button>
{/* AI Noise Suppression (RNNoise) */}
<button
onClick={toggleRnnoise}
className={`${btnBase} ${
rnnoiseEnabled
? 'bg-[#111214] text-discord-green hover:bg-[#1a1b1e]'
: btnDefaultStyle
}`}
title={rnnoiseEnabled ? 'Disable AI Noise Suppression' : 'Enable AI Noise Suppression'}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" opacity={rnnoiseEnabled ? 0.15 : 0.08} />
<path d="M12 1a2 2 0 012 2v1a2 2 0 01-4 0V3a2 2 0 012-2z" />
<path d="M12 7c-1.66 0-3 1.34-3 3v2c0 1.66 1.34 3 3 3s3-1.34 3-3v-2c0-1.66-1.34-3-3-3z" />
<path d="M17 11v1c0 2.76-2.24 5-5 5s-5-2.24-5-5v-1H5v1c0 3.53 2.61 6.43 6 6.92V21h2v-2.08c3.39-.49 6-3.39 6-6.92v-1h-2z" />
{rnnoiseEnabled ? (
<>
<circle cx="18" cy="5" r="1.2" fill="currentColor" />
<circle cx="20" cy="8" r="0.9" fill="currentColor" opacity="0.7" />
<circle cx="6" cy="5" r="1.2" fill="currentColor" />
<circle cx="4" cy="8" r="0.9" fill="currentColor" opacity="0.7" />
</>
) : (
<line x1="4" y1="4" x2="20" y2="20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" opacity="0.4" />
)}
</svg>
</button>
{/* Video Quality Popover */}
<VideoQualityPopover
open={showVideoQuality}