fix: address sender-side voice ducking with voice processing controls
Disable AGC by default to prevent Chrome from crushing mic sensitivity when stream audio is playing. Add user-facing toggles for echo cancellation, noise suppression, and auto gain control. Decouple voice and stream audio by routing through ctx.destination instead of shared compressor. Track mic stream generation to re-publish when settings change.
This commit is contained in:
@@ -12,6 +12,10 @@ export class AudioManager {
|
|||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
listeners = new Set();
|
listeners = new Set();
|
||||||
soundBuffers = new Map();
|
soundBuffers = new Map();
|
||||||
|
voiceEchoCancellation = true;
|
||||||
|
voiceNoiseSuppression = true;
|
||||||
|
voiceAutoGainControl = false;
|
||||||
|
streamGeneration = 0;
|
||||||
constructor() { }
|
constructor() { }
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
if (!AudioManager.instance) {
|
if (!AudioManager.instance) {
|
||||||
@@ -121,13 +125,14 @@ export class AudioManager {
|
|||||||
const constraints = {
|
const constraints = {
|
||||||
audio: {
|
audio: {
|
||||||
deviceId: deviceId === 'default' ? undefined : { exact: deviceId },
|
deviceId: deviceId === 'default' ? undefined : { exact: deviceId },
|
||||||
echoCancellation: true,
|
echoCancellation: this.voiceEchoCancellation,
|
||||||
noiseSuppression: true,
|
noiseSuppression: this.voiceNoiseSuppression,
|
||||||
autoGainControl: true
|
autoGainControl: this.voiceAutoGainControl,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
this.currentInputDeviceId = deviceId;
|
this.currentInputDeviceId = deviceId;
|
||||||
|
this.streamGeneration++;
|
||||||
if (this.ctx && this.inputGain) {
|
if (this.ctx && this.inputGain) {
|
||||||
if (this.inputSource) {
|
if (this.inputSource) {
|
||||||
this.inputSource.disconnect();
|
this.inputSource.disconnect();
|
||||||
@@ -150,6 +155,28 @@ export class AudioManager {
|
|||||||
this.inputGain.gain.setTargetAtTime(gainValue, this.ctx.currentTime, 0.1);
|
this.inputGain.gain.setTargetAtTime(gainValue, this.ctx.currentTime, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setVoiceProcessing(opts) {
|
||||||
|
let changed = false;
|
||||||
|
if (opts.echoCancellation !== undefined && opts.echoCancellation !== this.voiceEchoCancellation) {
|
||||||
|
this.voiceEchoCancellation = opts.echoCancellation;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (opts.noiseSuppression !== undefined && opts.noiseSuppression !== this.voiceNoiseSuppression) {
|
||||||
|
this.voiceNoiseSuppression = opts.noiseSuppression;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (opts.autoGainControl !== undefined && opts.autoGainControl !== this.voiceAutoGainControl) {
|
||||||
|
this.voiceAutoGainControl = opts.autoGainControl;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed && this.currentStream) {
|
||||||
|
this.currentStream.getTracks().forEach(t => t.stop());
|
||||||
|
this.currentStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getStreamGeneration() {
|
||||||
|
return this.streamGeneration;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* CRITICAL: Always returns a CLONE of the destination track.
|
* CRITICAL: Always returns a CLONE of the destination track.
|
||||||
* This prevents LiveKit's cleanup from killing the main singleton track
|
* This prevents LiveKit's cleanup from killing the main singleton track
|
||||||
@@ -190,7 +217,7 @@ export class AudioManager {
|
|||||||
getMasterOutput() {
|
getMasterOutput() {
|
||||||
if (!this.ctx)
|
if (!this.ctx)
|
||||||
this.initContext();
|
this.initContext();
|
||||||
return this.masterCompressor;
|
return this.ctx.destination;
|
||||||
}
|
}
|
||||||
getContext() {
|
getContext() {
|
||||||
return this.ctx;
|
return this.ctx;
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export class AudioManager {
|
|||||||
|
|
||||||
private listeners: Set<() => void> = new Set();
|
private listeners: Set<() => void> = new Set();
|
||||||
private soundBuffers: Map<string, AudioBuffer> = new Map();
|
private soundBuffers: Map<string, AudioBuffer> = new Map();
|
||||||
|
private voiceEchoCancellation = true;
|
||||||
|
private voiceNoiseSuppression = true;
|
||||||
|
private voiceAutoGainControl = false;
|
||||||
|
private streamGeneration = 0;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@@ -140,14 +144,15 @@ export class AudioManager {
|
|||||||
const constraints = {
|
const constraints = {
|
||||||
audio: {
|
audio: {
|
||||||
deviceId: deviceId === 'default' ? undefined : { exact: deviceId },
|
deviceId: deviceId === 'default' ? undefined : { exact: deviceId },
|
||||||
echoCancellation: true,
|
echoCancellation: this.voiceEchoCancellation,
|
||||||
noiseSuppression: true,
|
noiseSuppression: this.voiceNoiseSuppression,
|
||||||
autoGainControl: true
|
autoGainControl: this.voiceAutoGainControl,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
this.currentInputDeviceId = deviceId;
|
this.currentInputDeviceId = deviceId;
|
||||||
|
this.streamGeneration++;
|
||||||
|
|
||||||
if (this.ctx && this.inputGain) {
|
if (this.ctx && this.inputGain) {
|
||||||
if (this.inputSource) {
|
if (this.inputSource) {
|
||||||
@@ -172,6 +177,30 @@ export class AudioManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVoiceProcessing(opts: { echoCancellation?: boolean; noiseSuppression?: boolean; autoGainControl?: boolean }) {
|
||||||
|
let changed = false;
|
||||||
|
if (opts.echoCancellation !== undefined && opts.echoCancellation !== this.voiceEchoCancellation) {
|
||||||
|
this.voiceEchoCancellation = opts.echoCancellation;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (opts.noiseSuppression !== undefined && opts.noiseSuppression !== this.voiceNoiseSuppression) {
|
||||||
|
this.voiceNoiseSuppression = opts.noiseSuppression;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (opts.autoGainControl !== undefined && opts.autoGainControl !== this.voiceAutoGainControl) {
|
||||||
|
this.voiceAutoGainControl = opts.autoGainControl;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed && this.currentStream) {
|
||||||
|
this.currentStream.getTracks().forEach(t => t.stop());
|
||||||
|
this.currentStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreamGeneration(): number {
|
||||||
|
return this.streamGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CRITICAL: Always returns a CLONE of the destination track.
|
* CRITICAL: Always returns a CLONE of the destination track.
|
||||||
* This prevents LiveKit's cleanup from killing the main singleton track
|
* This prevents LiveKit's cleanup from killing the main singleton track
|
||||||
@@ -210,7 +239,7 @@ export class AudioManager {
|
|||||||
*/
|
*/
|
||||||
getMasterOutput(): AudioNode {
|
getMasterOutput(): AudioNode {
|
||||||
if (!this.ctx) this.initContext();
|
if (!this.ctx) this.initContext();
|
||||||
return this.masterCompressor!;
|
return this.ctx!.destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
getContext(): AudioContext | null {
|
getContext(): AudioContext | null {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { Modal } from '../ui/Modal';
|
import { Modal } from '../ui/Modal';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
import { Avatar } from '../ui/Avatar';
|
import { Avatar } from '../ui/Avatar';
|
||||||
|
|
||||||
export function UserSettingsModal() {
|
export function UserSettingsModal() {
|
||||||
@@ -18,6 +19,13 @@ export function UserSettingsModal() {
|
|||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const echoCancellation = useVoiceStore((s) => s.echoCancellation);
|
||||||
|
const noiseSuppression = useVoiceStore((s) => s.noiseSuppression);
|
||||||
|
const autoGainControl = useVoiceStore((s) => s.autoGainControl);
|
||||||
|
const setEchoCancellation = useVoiceStore((s) => s.setEchoCancellation);
|
||||||
|
const toggleNoiseSuppression = useVoiceStore((s) => s.toggleNoiseSuppression);
|
||||||
|
const setAutoGainControl = useVoiceStore((s) => s.setAutoGainControl);
|
||||||
|
|
||||||
const isOpen = activeModal === 'userSettings';
|
const isOpen = activeModal === 'userSettings';
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -113,6 +121,55 @@ export function UserSettingsModal() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Voice Processing */}
|
||||||
|
<div className="border-t border-white/[0.06] pt-4">
|
||||||
|
<h3 className="text-xs font-bold text-discord-text-secondary uppercase mb-3">
|
||||||
|
Voice Processing
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-discord-text-muted mb-3">
|
||||||
|
Disable Auto Gain Control when streaming to prevent your browser from ducking your microphone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-discord-text-primary">Echo Cancellation</div>
|
||||||
|
<div className="text-xs text-discord-text-muted">Removes echo when using speakers</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setEchoCancellation(!echoCancellation)}
|
||||||
|
className={`relative w-10 h-5 rounded-full transition-colors ${echoCancellation ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`}
|
||||||
|
>
|
||||||
|
<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'}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-discord-text-primary">Noise Suppression</div>
|
||||||
|
<div className="text-xs text-discord-text-muted">Filters background noise from your mic</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={toggleNoiseSuppression}
|
||||||
|
className={`relative w-10 h-5 rounded-full transition-colors ${noiseSuppression ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`}
|
||||||
|
>
|
||||||
|
<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'}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-discord-text-primary">Auto Gain Control</div>
|
||||||
|
<div className="text-xs text-discord-text-muted">Auto-adjusts mic volume — can cause voice ducking during streams</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setAutoGainControl(!autoGainControl)}
|
||||||
|
className={`relative w-10 h-5 rounded-full transition-colors ${autoGainControl ? 'bg-discord-green' : 'bg-discord-bg-tertiary'}`}
|
||||||
|
>
|
||||||
|
<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'}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-2">
|
<div className="flex items-center justify-between pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ export function useLiveKit() {
|
|||||||
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||||
const inputVolume = useVoiceStore((s) => s.inputVolume);
|
const inputVolume = useVoiceStore((s) => s.inputVolume);
|
||||||
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
|
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
|
||||||
|
const echoCancellation = useVoiceStore((s) => s.echoCancellation);
|
||||||
|
const noiseSuppression = useVoiceStore((s) => s.noiseSuppression);
|
||||||
|
const autoGainControl = useVoiceStore((s) => s.autoGainControl);
|
||||||
|
const lastMicGenRef = useRef(0);
|
||||||
const updateParticipants = useCallback(() => {
|
const updateParticipants = useCallback(() => {
|
||||||
const r = roomRef.current;
|
const r = roomRef.current;
|
||||||
if (!r)
|
if (!r)
|
||||||
@@ -192,6 +196,8 @@ export function useLiveKit() {
|
|||||||
const syncMic = async () => {
|
const syncMic = async () => {
|
||||||
try {
|
try {
|
||||||
const audioManager = AudioManager.getInstance();
|
const audioManager = AudioManager.getInstance();
|
||||||
|
// Sync voice processing settings to AudioManager
|
||||||
|
audioManager.setVoiceProcessing({ echoCancellation, noiseSuppression, autoGainControl });
|
||||||
// If muted or deafened, unpublish mic
|
// If muted or deafened, unpublish mic
|
||||||
if (isMuted || isDeafened) {
|
if (isMuted || isDeafened) {
|
||||||
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
||||||
@@ -203,25 +209,27 @@ export function useLiveKit() {
|
|||||||
// Ensure device is set and volume is sync'd
|
// Ensure device is set and volume is sync'd
|
||||||
await audioManager.setInputDevice(inputDeviceId);
|
await audioManager.setInputDevice(inputDeviceId);
|
||||||
audioManager.setInputVolume(inputVolume);
|
audioManager.setInputVolume(inputVolume);
|
||||||
|
const currentGen = audioManager.getStreamGeneration();
|
||||||
// Check if already published
|
// Check if already published
|
||||||
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
||||||
if (existingPub && existingPub.track) {
|
if (existingPub && existingPub.track) {
|
||||||
// If track is alive, we are good.
|
// If track is alive AND settings haven't changed, we are good.
|
||||||
if (existingPub.track.mediaStreamTrack?.readyState === 'live') {
|
if (existingPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If track died, unpublish so we can republish
|
// Settings changed or track died — unpublish so we can republish
|
||||||
await r.localParticipant.unpublishTrack(existingPub.track);
|
await r.localParticipant.unpublishTrack(existingPub.track);
|
||||||
}
|
}
|
||||||
// Get a FRESH track (clone) for this specific publication
|
// Get a FRESH track (clone) for this specific publication
|
||||||
const audioTrack = audioManager.getFreshTrack();
|
const audioTrack = audioManager.getFreshTrack();
|
||||||
if (!audioTrack)
|
if (!audioTrack)
|
||||||
return;
|
return;
|
||||||
console.log('[LiveKit] Publishing fresh microphone track');
|
console.log('[LiveKit] Publishing fresh microphone track (gen:', currentGen, ')');
|
||||||
await r.localParticipant.publishTrack(audioTrack, {
|
await r.localParticipant.publishTrack(audioTrack, {
|
||||||
name: 'microphone',
|
name: 'microphone',
|
||||||
source: Track.Source.Microphone,
|
source: Track.Source.Microphone,
|
||||||
});
|
});
|
||||||
|
lastMicGenRef.current = currentGen;
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('[LiveKit] Failed to sync mic state:', err);
|
console.error('[LiveKit] Failed to sync mic state:', err);
|
||||||
@@ -235,7 +243,7 @@ export function useLiveKit() {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected]);
|
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected, echoCancellation, noiseSuppression, autoGainControl]);
|
||||||
const connect = useCallback(async (channelId) => {
|
const connect = useCallback(async (channelId) => {
|
||||||
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected)
|
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ export function useLiveKit() {
|
|||||||
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||||
const inputVolume = useVoiceStore((s) => s.inputVolume);
|
const inputVolume = useVoiceStore((s) => s.inputVolume);
|
||||||
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
|
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
|
||||||
|
const echoCancellation = useVoiceStore((s) => s.echoCancellation);
|
||||||
|
const noiseSuppression = useVoiceStore((s) => s.noiseSuppression);
|
||||||
|
const autoGainControl = useVoiceStore((s) => s.autoGainControl);
|
||||||
|
|
||||||
|
const lastMicGenRef = useRef(0);
|
||||||
|
|
||||||
const updateParticipants = useCallback(() => {
|
const updateParticipants = useCallback(() => {
|
||||||
const r = roomRef.current;
|
const r = roomRef.current;
|
||||||
@@ -243,7 +248,10 @@ export function useLiveKit() {
|
|||||||
const syncMic = async () => {
|
const syncMic = async () => {
|
||||||
try {
|
try {
|
||||||
const audioManager = AudioManager.getInstance();
|
const audioManager = AudioManager.getInstance();
|
||||||
|
|
||||||
|
// Sync voice processing settings to AudioManager
|
||||||
|
audioManager.setVoiceProcessing({ echoCancellation, noiseSuppression, autoGainControl });
|
||||||
|
|
||||||
// If muted or deafened, unpublish mic
|
// If muted or deafened, unpublish mic
|
||||||
if (isMuted || isDeafened) {
|
if (isMuted || isDeafened) {
|
||||||
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
||||||
@@ -256,16 +264,18 @@ export function useLiveKit() {
|
|||||||
// Ensure device is set and volume is sync'd
|
// Ensure device is set and volume is sync'd
|
||||||
await audioManager.setInputDevice(inputDeviceId);
|
await audioManager.setInputDevice(inputDeviceId);
|
||||||
audioManager.setInputVolume(inputVolume);
|
audioManager.setInputVolume(inputVolume);
|
||||||
|
|
||||||
|
const currentGen = audioManager.getStreamGeneration();
|
||||||
|
|
||||||
// Check if already published
|
// Check if already published
|
||||||
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
||||||
|
|
||||||
if (existingPub && existingPub.track) {
|
if (existingPub && existingPub.track) {
|
||||||
// If track is alive, we are good.
|
// If track is alive AND settings haven't changed, we are good.
|
||||||
if (existingPub.track.mediaStreamTrack?.readyState === 'live') {
|
if (existingPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If track died, unpublish so we can republish
|
// Settings changed or track died — unpublish so we can republish
|
||||||
await r.localParticipant.unpublishTrack(existingPub.track as LocalAudioTrack);
|
await r.localParticipant.unpublishTrack(existingPub.track as LocalAudioTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,12 +283,13 @@ export function useLiveKit() {
|
|||||||
const audioTrack = audioManager.getFreshTrack();
|
const audioTrack = audioManager.getFreshTrack();
|
||||||
if (!audioTrack) return;
|
if (!audioTrack) return;
|
||||||
|
|
||||||
console.log('[LiveKit] Publishing fresh microphone track');
|
console.log('[LiveKit] Publishing fresh microphone track (gen:', currentGen, ')');
|
||||||
await r.localParticipant.publishTrack(audioTrack, {
|
await r.localParticipant.publishTrack(audioTrack, {
|
||||||
name: 'microphone',
|
name: 'microphone',
|
||||||
source: Track.Source.Microphone,
|
source: Track.Source.Microphone,
|
||||||
});
|
});
|
||||||
|
lastMicGenRef.current = currentGen;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[LiveKit] Failed to sync mic state:', err);
|
console.error('[LiveKit] Failed to sync mic state:', err);
|
||||||
}
|
}
|
||||||
@@ -290,11 +301,11 @@ export function useLiveKit() {
|
|||||||
const unsubscribe = AudioManager.getInstance().onResumed(() => {
|
const unsubscribe = AudioManager.getInstance().onResumed(() => {
|
||||||
syncMic();
|
syncMic();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected]);
|
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected, echoCancellation, noiseSuppression, autoGainControl]);
|
||||||
|
|
||||||
const connect = useCallback(async (channelId: string) => {
|
const connect = useCallback(async (channelId: string) => {
|
||||||
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected) return;
|
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected) return;
|
||||||
|
|||||||
@@ -131,7 +131,11 @@ export const useVoiceStore = create()(persist((set, get) => ({
|
|||||||
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
|
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
|
||||||
setVideoQuality: (quality) => set({ videoQuality: quality }),
|
setVideoQuality: (quality) => set({ videoQuality: quality }),
|
||||||
noiseSuppression: true,
|
noiseSuppression: true,
|
||||||
|
echoCancellation: true,
|
||||||
|
autoGainControl: false,
|
||||||
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
|
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
|
||||||
|
setEchoCancellation: (enabled) => set({ echoCancellation: enabled }),
|
||||||
|
setAutoGainControl: (enabled) => set({ autoGainControl: enabled }),
|
||||||
deafenedUserIds: new Set(),
|
deafenedUserIds: new Set(),
|
||||||
setUserDeafened: (userId, deafened) => {
|
setUserDeafened: (userId, deafened) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -203,11 +207,15 @@ export const useVoiceStore = create()(persist((set, get) => ({
|
|||||||
}),
|
}),
|
||||||
}), {
|
}), {
|
||||||
name: 'opencord-voice-settings',
|
name: 'opencord-voice-settings',
|
||||||
version: 1,
|
version: 2,
|
||||||
migrate: (persistedState, version) => {
|
migrate: (persistedState, version) => {
|
||||||
if (version === 0) {
|
if (version === 0) {
|
||||||
persistedState.streamAttenuationEnabled = false;
|
persistedState.streamAttenuationEnabled = false;
|
||||||
}
|
}
|
||||||
|
if (version < 2) {
|
||||||
|
persistedState.echoCancellation = true;
|
||||||
|
persistedState.autoGainControl = false;
|
||||||
|
}
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
@@ -222,6 +230,8 @@ export const useVoiceStore = create()(persist((set, get) => ({
|
|||||||
outputDeviceId: state.outputDeviceId,
|
outputDeviceId: state.outputDeviceId,
|
||||||
videoQuality: state.videoQuality,
|
videoQuality: state.videoQuality,
|
||||||
noiseSuppression: state.noiseSuppression,
|
noiseSuppression: state.noiseSuppression,
|
||||||
|
echoCancellation: state.echoCancellation,
|
||||||
|
autoGainControl: state.autoGainControl,
|
||||||
streamAttenuationEnabled: state.streamAttenuationEnabled,
|
streamAttenuationEnabled: state.streamAttenuationEnabled,
|
||||||
streamAttenuationStrength: state.streamAttenuationStrength,
|
streamAttenuationStrength: state.streamAttenuationStrength,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ interface VoiceState {
|
|||||||
setFocusedParticipant: (id: string | null) => void;
|
setFocusedParticipant: (id: string | null) => void;
|
||||||
setVideoQuality: (quality: '1080p' | '1080p60' | '720p' | '720p60' | '540p' | '360p') => void;
|
setVideoQuality: (quality: '1080p' | '1080p60' | '720p' | '720p60' | '540p' | '360p') => void;
|
||||||
noiseSuppression: boolean;
|
noiseSuppression: boolean;
|
||||||
|
echoCancellation: boolean;
|
||||||
|
autoGainControl: boolean;
|
||||||
toggleNoiseSuppression: () => void;
|
toggleNoiseSuppression: () => void;
|
||||||
|
setEchoCancellation: (enabled: boolean) => void;
|
||||||
|
setAutoGainControl: (enabled: boolean) => void;
|
||||||
deafenedUserIds: Set<string>;
|
deafenedUserIds: Set<string>;
|
||||||
setUserDeafened: (userId: string, deafened: boolean) => void;
|
setUserDeafened: (userId: string, deafened: boolean) => void;
|
||||||
// WebSocket-based voice user status (visible without joining LiveKit)
|
// WebSocket-based voice user status (visible without joining LiveKit)
|
||||||
@@ -221,7 +225,11 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
|
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
|
||||||
setVideoQuality: (quality) => set({ videoQuality: quality }),
|
setVideoQuality: (quality) => set({ videoQuality: quality }),
|
||||||
noiseSuppression: true,
|
noiseSuppression: true,
|
||||||
|
echoCancellation: true,
|
||||||
|
autoGainControl: false,
|
||||||
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
|
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
|
||||||
|
setEchoCancellation: (enabled) => set({ echoCancellation: enabled }),
|
||||||
|
setAutoGainControl: (enabled) => set({ autoGainControl: enabled }),
|
||||||
deafenedUserIds: new Set(),
|
deafenedUserIds: new Set(),
|
||||||
setUserDeafened: (userId, deafened) => {
|
setUserDeafened: (userId, deafened) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -296,11 +304,15 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'opencord-voice-settings',
|
name: 'opencord-voice-settings',
|
||||||
version: 1,
|
version: 2,
|
||||||
migrate: (persistedState: any, version: number) => {
|
migrate: (persistedState: any, version: number) => {
|
||||||
if (version === 0) {
|
if (version === 0) {
|
||||||
persistedState.streamAttenuationEnabled = false;
|
persistedState.streamAttenuationEnabled = false;
|
||||||
}
|
}
|
||||||
|
if (version < 2) {
|
||||||
|
persistedState.echoCancellation = true;
|
||||||
|
persistedState.autoGainControl = false;
|
||||||
|
}
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
@@ -315,6 +327,8 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
outputDeviceId: state.outputDeviceId,
|
outputDeviceId: state.outputDeviceId,
|
||||||
videoQuality: state.videoQuality,
|
videoQuality: state.videoQuality,
|
||||||
noiseSuppression: state.noiseSuppression,
|
noiseSuppression: state.noiseSuppression,
|
||||||
|
echoCancellation: state.echoCancellation,
|
||||||
|
autoGainControl: state.autoGainControl,
|
||||||
streamAttenuationEnabled: state.streamAttenuationEnabled,
|
streamAttenuationEnabled: state.streamAttenuationEnabled,
|
||||||
streamAttenuationStrength: state.streamAttenuationStrength,
|
streamAttenuationStrength: state.streamAttenuationStrength,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user