From 5747267a6b9a7bd6e2dee373de7db74fd0c36f83 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:49:42 +0100 Subject: [PATCH] refactor: remove browser NS toggle, default RNNoise on, rework voice settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RNNoise (AI Noise Suppression) now enabled by default for all users - Remove redundant browser Noise Suppression toggle from VoiceControls and UserSettings — AudioManager handles it automatically as fallback - Add AI Noise Suppression toggle to UserSettings panel - Rename toggleRnnoise → setRnnoiseEnabled for clearer API - Store migration v3→v4: enable RNNoise for existing users - Keep Echo Cancellation and Auto Gain Control (orthogonal features) --- .../web/src/components/modals/UserSettings.js | 6 ++-- .../src/components/modals/UserSettings.tsx | 33 ++++++++---------- .../web/src/components/voice/VoiceControls.js | 11 ++---- .../src/components/voice/VoiceControls.tsx | 34 ++----------------- packages/web/src/stores/voiceStore.js | 15 ++++---- packages/web/src/stores/voiceStore.ts | 20 ++++++----- 6 files changed, 41 insertions(+), 78 deletions(-) diff --git a/packages/web/src/components/modals/UserSettings.js b/packages/web/src/components/modals/UserSettings.js index f14f2d8b..7983cde9 100644 --- a/packages/web/src/components/modals/UserSettings.js +++ b/packages/web/src/components/modals/UserSettings.js @@ -18,11 +18,11 @@ export function UserSettingsModal() { const [success, setSuccess] = useState(''); const [isLoading, setIsLoading] = useState(false); const echoCancellation = useVoiceStore((s) => s.echoCancellation); - const noiseSuppression = useVoiceStore((s) => s.noiseSuppression); const autoGainControl = useVoiceStore((s) => s.autoGainControl); + const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled); const setEchoCancellation = useVoiceStore((s) => s.setEchoCancellation); - const toggleNoiseSuppression = useVoiceStore((s) => s.toggleNoiseSuppression); const setAutoGainControl = useVoiceStore((s) => s.setAutoGainControl); + const setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled); const isOpen = activeModal === 'userSettings'; const handleSave = async () => { setError(''); @@ -50,5 +50,5 @@ export function UserSettingsModal() { }; if (!user) return null; - return (_jsx(Modal, { isOpen: isOpen, onClose: closeModal, title: "User Settings", maxWidth: "max-w-lg", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center gap-4 p-4 bg-discord-bg-secondary rounded-lg", children: [_jsx(Avatar, { src: user.avatar, name: user.displayName ?? user.username, size: 64, status: user.status }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-lg", children: user.displayName ?? user.username }), _jsxs("div", { className: "text-discord-text-muted text-sm", children: ["@", user.username] }), user.customStatus && (_jsx("div", { className: "text-discord-text-secondary text-sm mt-1", children: user.customStatus }))] })] }), error && (_jsx("div", { className: "p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-text-danger text-sm", children: error })), success && (_jsx("div", { className: "p-2 bg-discord-green/10 border border-discord-green/30 rounded text-discord-text-positive text-sm", children: success })), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Status" }), _jsxs("select", { value: status, onChange: (e) => setStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple appearance-none", children: [_jsx("option", { value: "online", children: "Online" }), _jsx("option", { value: "idle", children: "Idle" }), _jsx("option", { value: "dnd", children: "Do Not Disturb" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Display Name" }), _jsx("input", { type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Custom Status" }), _jsx("input", { type: "text", value: customStatus, onChange: (e) => setCustomStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple", placeholder: "What are you up to?" })] }), _jsxs("div", { className: "border-t border-white/[0.06] pt-4", children: [_jsx("h3", { className: "text-xs font-bold text-discord-text-secondary uppercase mb-3", children: "Voice Processing" }), _jsx("p", { className: "text-xs text-discord-text-muted mb-3", children: "Disable Auto Gain Control when streaming to prevent your browser from ducking your microphone." }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-discord-text-primary", children: "Echo Cancellation" }), _jsx("div", { className: "text-xs text-discord-text-muted", children: "Removes echo when using speakers" })] }), _jsx("button", { onClick: () => setEchoCancellation(!echoCancellation), className: `relative w-10 h-5 rounded-full transition-colors ${echoCancellation ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`, children: _jsx("div", { className: `absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${echoCancellation ? 'translate-x-5' : 'translate-x-0'}` }) })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-discord-text-primary", children: "Noise Suppression" }), _jsx("div", { className: "text-xs text-discord-text-muted", children: "Filters background noise from your mic" })] }), _jsx("button", { onClick: toggleNoiseSuppression, className: `relative w-10 h-5 rounded-full transition-colors ${noiseSuppression ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`, children: _jsx("div", { className: `absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${noiseSuppression ? 'translate-x-5' : 'translate-x-0'}` }) })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-discord-text-primary", children: "Auto Gain Control" }), _jsx("div", { className: "text-xs text-discord-text-muted", children: "Auto-adjusts mic volume \u2014 can cause voice ducking during streams" })] }), _jsx("button", { onClick: () => setAutoGainControl(!autoGainControl), className: `relative w-10 h-5 rounded-full transition-colors ${autoGainControl ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`, children: _jsx("div", { className: `absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${autoGainControl ? 'translate-x-5' : 'translate-x-0'}` }) })] })] }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx("button", { onClick: handleLogout, className: "px-4 py-2 text-sm text-discord-red hover:bg-discord-red/10 rounded transition-colors", children: "Log Out" }), _jsx("button", { onClick: handleSave, disabled: isLoading, className: "px-4 py-2 bg-discord-blurple hover:bg-discord-blurple-hover text-white text-sm font-medium rounded transition-colors disabled:opacity-50", children: isLoading ? 'Saving...' : 'Save Changes' })] })] }) })); + return (_jsx(Modal, { isOpen: isOpen, onClose: closeModal, title: "User Settings", maxWidth: "max-w-lg", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center gap-4 p-4 bg-discord-bg-secondary rounded-lg", children: [_jsx(Avatar, { src: user.avatar, name: user.displayName ?? user.username, size: 64, status: user.status }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-lg", children: user.displayName ?? user.username }), _jsxs("div", { className: "text-discord-text-muted text-sm", children: ["@", user.username] }), user.customStatus && (_jsx("div", { className: "text-discord-text-secondary text-sm mt-1", children: user.customStatus }))] })] }), error && (_jsx("div", { className: "p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-text-danger text-sm", children: error })), success && (_jsx("div", { className: "p-2 bg-discord-green/10 border border-discord-green/30 rounded text-discord-text-positive text-sm", children: success })), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Status" }), _jsxs("select", { value: status, onChange: (e) => setStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple appearance-none", children: [_jsx("option", { value: "online", children: "Online" }), _jsx("option", { value: "idle", children: "Idle" }), _jsx("option", { value: "dnd", children: "Do Not Disturb" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Display Name" }), _jsx("input", { type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Custom Status" }), _jsx("input", { type: "text", value: customStatus, onChange: (e) => setCustomStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple", placeholder: "What are you up to?" })] }), _jsxs("div", { className: "border-t border-white/[0.06] pt-4", children: [_jsx("h3", { className: "text-xs font-bold text-discord-text-secondary uppercase mb-3", children: "Voice Processing" }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-discord-text-primary", children: "AI Noise Suppression" }), _jsx("div", { className: "text-xs text-discord-text-muted", children: "ML-based noise removal (RNNoise) \u2014 filters keyboard, fans, and background noise" })] }), _jsx("button", { onClick: () => setRnnoiseEnabled(!rnnoiseEnabled), className: `relative w-10 h-5 rounded-full transition-colors ${rnnoiseEnabled ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`, children: _jsx("div", { className: `absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${rnnoiseEnabled ? 'translate-x-5' : 'translate-x-0'}` }) })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-discord-text-primary", children: "Echo Cancellation" }), _jsx("div", { className: "text-xs text-discord-text-muted", children: "Removes echo when using speakers (auto-disabled during screen share)" })] }), _jsx("button", { onClick: () => setEchoCancellation(!echoCancellation), className: `relative w-10 h-5 rounded-full transition-colors ${echoCancellation ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`, children: _jsx("div", { className: `absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${echoCancellation ? 'translate-x-5' : 'translate-x-0'}` }) })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-discord-text-primary", children: "Auto Gain Control" }), _jsx("div", { className: "text-xs text-discord-text-muted", children: "Auto-adjusts mic volume \u2014 can cause voice ducking during streams" })] }), _jsx("button", { onClick: () => setAutoGainControl(!autoGainControl), className: `relative w-10 h-5 rounded-full transition-colors ${autoGainControl ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`, children: _jsx("div", { className: `absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${autoGainControl ? 'translate-x-5' : 'translate-x-0'}` }) })] })] }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx("button", { onClick: handleLogout, className: "px-4 py-2 text-sm text-discord-red hover:bg-discord-red/10 rounded transition-colors", children: "Log Out" }), _jsx("button", { onClick: handleSave, disabled: isLoading, className: "px-4 py-2 bg-discord-blurple hover:bg-discord-blurple-hover text-white text-sm font-medium rounded transition-colors disabled:opacity-50", children: isLoading ? 'Saving...' : 'Save Changes' })] })] }) })); } diff --git a/packages/web/src/components/modals/UserSettings.tsx b/packages/web/src/components/modals/UserSettings.tsx index c2f41cc2..3f356a54 100644 --- a/packages/web/src/components/modals/UserSettings.tsx +++ b/packages/web/src/components/modals/UserSettings.tsx @@ -20,11 +20,11 @@ export function UserSettingsModal() { const [isLoading, setIsLoading] = useState(false); const echoCancellation = useVoiceStore((s) => s.echoCancellation); - const noiseSuppression = useVoiceStore((s) => s.noiseSuppression); const autoGainControl = useVoiceStore((s) => s.autoGainControl); + const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled); const setEchoCancellation = useVoiceStore((s) => s.setEchoCancellation); - const toggleNoiseSuppression = useVoiceStore((s) => s.toggleNoiseSuppression); const setAutoGainControl = useVoiceStore((s) => s.setAutoGainControl); + const setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled); const isOpen = activeModal === 'userSettings'; @@ -126,9 +126,19 @@ export function UserSettingsModal() {

Voice Processing

-

- Echo Cancellation is automatically disabled while you screen share to prevent Chrome from ducking your mic volume. -

+ +
+
+
AI Noise Suppression
+
ML-based noise removal (RNNoise) — filters keyboard, fans, and background noise
+
+ +
@@ -143,19 +153,6 @@ export function UserSettingsModal() {
-
-
-
Noise Suppression
-
Filters background noise from your mic
-
- -
-
Auto Gain Control
diff --git a/packages/web/src/components/voice/VoiceControls.js b/packages/web/src/components/voice/VoiceControls.js index ca66b342..0782ff3b 100644 --- a/packages/web/src/components/voice/VoiceControls.js +++ b/packages/web/src/components/voice/VoiceControls.js @@ -15,10 +15,8 @@ export function VoiceControls() { const isScreenSharing = useVoiceStore((s) => s.isScreenSharing); const toggleCamera = useVoiceStore((s) => s.toggleCamera); 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 setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled); const connectionError = useVoiceStore((s) => s.connectionError); const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected); const channels = useServerStore((s) => s.channels); @@ -56,9 +54,6 @@ export function VoiceControls() { console.error('[VoiceControls] Failed to toggle screen share:', err); } }; - const handleNoiseSuppression = () => { - toggleNoiseSuppression(); - }; const handleDisconnect = () => { wsSend({ type: 'voice_leave' }); useVoiceStore.getState().leaveVoice(); @@ -81,9 +76,7 @@ export function VoiceControls() { ? 'bg-[#111214] text-discord-green hover:bg-[#1a1b1e]' : btnDefaultStyle}`, title: isScreenSharing ? 'Stop Sharing' : 'Share Screen', children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("path", { d: "M20 18C21.1 18 22 17.1 22 16V6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V16C2 17.1 2.9 18 4 18H0V20H24V18H20ZM4 6H20V16H4V6Z" }), _jsx("path", { d: "M15 11L11 14V12H9V10H11V8L15 11Z" })] }) }), _jsx("button", { onClick: () => setShowVideoQuality(!showVideoQuality), className: `${btnBase} ${showVideoQuality ? 'bg-[#111214] text-discord-blurple hover:bg-[#1a1b1e]' - : btnDefaultStyle}`, title: "Video Quality", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("path", { d: "M3 5v14h18V5H3zm16 12H5V7h14v10z" }), _jsx("path", { d: "M8 15l2.5-3.21L13 15l2-2.5L18 17H6z" })] }) }), _jsx("button", { onClick: handleNoiseSuppression, className: `${btnBase} ${noiseSuppression - ? 'bg-[#111214] text-discord-green hover:bg-[#1a1b1e]' - : btnDefaultStyle}`, title: noiseSuppression ? 'Disable Browser Noise Suppression' : 'Enable Browser Noise Suppression', children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("path", { d: "M7 9v6h4l5 5V4l-5 5H7z" }), noiseSuppression ? (_jsxs(_Fragment, { children: [_jsx("path", { d: "M19 12c0-1.66-.68-3.16-1.76-4.24l-1.42 1.42C16.55 9.9 17 10.9 17 12c0 1.1-.45 2.1-1.18 2.82l1.42 1.42C18.32 15.16 19 13.66 19 12z" }), _jsx("path", { d: "M21 12c0-2.76-1.12-5.26-2.93-7.07l-1.42 1.42C18.2 7.9 19 9.85 19 12c0 2.15-.8 4.1-2.35 5.65l1.42 1.42C19.88 17.26 21 14.76 21 12z", opacity: "0.6" })] })) : (_jsx("line", { x1: "19", y1: "5", x2: "19", y2: "19", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", opacity: "0.4" }))] }) }), _jsx("button", { onClick: toggleRnnoise, className: `${btnBase} ${rnnoiseEnabled + : btnDefaultStyle}`, title: "Video Quality", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("path", { d: "M3 5v14h18V5H3zm16 12H5V7h14v10z" }), _jsx("path", { d: "M8 15l2.5-3.21L13 15l2-2.5L18 17H6z" })] }) }), _jsx("button", { onClick: () => setRnnoiseEnabled(!rnnoiseEnabled), className: `${btnBase} ${rnnoiseEnabled ? 'bg-[#111214] text-discord-green hover:bg-[#1a1b1e]' : btnDefaultStyle}`, title: rnnoiseEnabled ? 'Disable AI Noise Suppression' : 'Enable AI Noise Suppression', children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("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 }), _jsx("path", { d: "M12 1a2 2 0 012 2v1a2 2 0 01-4 0V3a2 2 0 012-2z" }), _jsx("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" }), _jsx("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 ? (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "18", cy: "5", r: "1.2", fill: "currentColor" }), _jsx("circle", { cx: "20", cy: "8", r: "0.9", fill: "currentColor", opacity: "0.7" }), _jsx("circle", { cx: "6", cy: "5", r: "1.2", fill: "currentColor" }), _jsx("circle", { cx: "4", cy: "8", r: "0.9", fill: "currentColor", opacity: "0.7" })] })) : (_jsx("line", { x1: "4", y1: "4", x2: "20", y2: "20", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", opacity: "0.4" }))] }) }), _jsx(VideoQualityPopover, { open: showVideoQuality, onClose: () => setShowVideoQuality(false) })] })] })); } diff --git a/packages/web/src/components/voice/VoiceControls.tsx b/packages/web/src/components/voice/VoiceControls.tsx index 3cb3dbc7..cae80a8d 100644 --- a/packages/web/src/components/voice/VoiceControls.tsx +++ b/packages/web/src/components/voice/VoiceControls.tsx @@ -16,10 +16,8 @@ export function VoiceControls() { const isScreenSharing = useVoiceStore((s) => s.isScreenSharing); const toggleCamera = useVoiceStore((s) => s.toggleCamera); 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 setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled); const connectionError = useVoiceStore((s) => s.connectionError); const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected); const channels = useServerStore((s) => s.channels); @@ -58,11 +56,6 @@ export function VoiceControls() { } }; - const handleNoiseSuppression = () => { - // Store toggle triggers syncMic → AudioManager re-acquires stream with correct constraints - toggleNoiseSuppression(); - }; - const handleDisconnect = () => { wsSend({ type: 'voice_leave' }); useVoiceStore.getState().leaveVoice(); @@ -174,32 +167,9 @@ export function VoiceControls() { - {/* Browser Noise Suppression */} - - {/* AI Noise Suppression (RNNoise) */}