mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(huddle): stop 20 Hz speaker-level churn from re-rendering the whole app (#5825)
## Problem With a huddle open, Buzz Desktop becomes extremely slow and laggy (Tyler, live report, 2026-08-14). Root-caused and runtime-convicted on the instrumented rig in #buzz-conversational-agents: - The Rust playout loop emits `huddle-speaker-levels` over Tauri IPC every 50 ms, unconditionally, for the whole life of a huddle (`playout.rs` `SPEAKER_LEVEL_TICK_MS = 50`). - Each event deserializes to a fresh object, so `setRemoteSpeakerLevels` updates state at 20 Hz even in silence. - `HuddleProvider` wraps the entire main app and its context value was an inline object literal — never memoized. Every level tick minted a new context identity, re-rendering **every** `useHuddle()` consumer, including `ChannelScreen` and message rows. **Measured (A/B, silent one-participant huddle, same channel/state):** ~41 sustained ChannelScreen renders/sec unsuppressed vs ~4/sec with only the speaker-level setState suppressed — the 20 Hz path is ~90% of the load. Receipts: `driver-render-counter-unsuppressed.jsonl` / `-suppressed.jsonl` on the rig, verified independently. The same main-thread churn starves the relay client's 16 ms event-flush timer, which is the delayed/bursty message hydration and thread-panel stalls seen alongside the lag. ## Fix (minimal, no behavior change for meters) 1. **Split the high-frequency fields** (`micLevel`, `activeSpeakers`, `speakerLevels`) out of `HuddleContextValue` into a new `HuddleLevelsContext`, consumed via `useHuddleLevels()` only by the three meter components (`HuddleBar`, `HuddleRoomHeader`, `HuddleProfileControl`). 2. **Memoize the main context value** so provider re-renders no longer mint a new identity for the ~everything that consumes `useHuddle()`. 3. **Extract the mic-level analyser** into `useMicLevelAnalyser` — the level pipeline now lives in one place, and `HuddleContext.tsx` stays under the file-size ratchet (977 lines). Level meters keep their 20-30 Hz updates. Everything else re-renders only when a value it actually consumes changes. ## Acceptance bar With this fix, a silent open huddle should hold `ChannelScreen` at idle render rates (single digits/sec), and message hydration should stay live during huddles. The rig's render-counter + four-clock instrumentation can verify on this branch. ## Validation - `pnpm typecheck` clean - `biome check` clean (repo leftovers in sidebar tests are preexisting on main) - full desktop suite: **4,775 passed, 0 failed** at the final tree - file-size ratchet passes (was the reason for the analyser extraction) - lefthook pre-commit (desktop-fix + signoff) passed on commit Not yet done: live-local A/B rerun on this branch — the rig (Wren/Max) has the instrumentation ready and can convict/acquit the fix with the same probe that convicted the bug. Base: `068a83b0` (main). Co-developed with runtime evidence from Wren and instrumentation by Max. Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -11,8 +11,12 @@ import {
|
||||
useHuddlePttState,
|
||||
} from "./lib/useHuddlePttState";
|
||||
import { useHuddleSpeakerActivity } from "./lib/useHuddleSpeakerActivity";
|
||||
import { useMicLevelAnalyser } from "./lib/useMicLevelAnalyser";
|
||||
import { useTtsSubscription } from "./lib/useTtsSubscription";
|
||||
import type { HuddleContextValue } from "./HuddleContext.types";
|
||||
import type {
|
||||
HuddleContextValue,
|
||||
HuddleLevelsValue,
|
||||
} from "./HuddleContext.types";
|
||||
|
||||
/**
|
||||
* Huddle lifecycle (React context):
|
||||
@@ -47,29 +51,16 @@ const HUDDLE_AUDIO_COMMAND_EVENT = "huddle-audio-command";
|
||||
const HUDDLE_AUDIO_STATE_EVENT = "huddle-audio-state";
|
||||
const HUDDLE_AUDIO_LEVEL_EVENT = "huddle-audio-level";
|
||||
|
||||
const MIC_ANALYSER_UPDATE_INTERVAL_MS = 33;
|
||||
const MIC_INITIAL_NOISE_FLOOR = 0.01;
|
||||
const MIC_VOICE_GATE_ON_RMS = 0.018;
|
||||
const MIC_VOICE_GATE_OFF_RMS = 0.012;
|
||||
const MIC_VOICE_GATE_MARGIN_RMS = 0.012;
|
||||
const MIC_LEVEL_ACTIVE_RANGE_RMS = 0.11;
|
||||
const MIC_MIN_ACTIVE_LEVEL = 0.18;
|
||||
const MIC_LEVEL_ATTACK = 0.58;
|
||||
const MIC_ACTIVE_NOISE_FLOOR_RISE = 0.006;
|
||||
|
||||
function isRedundantHuddlePhaseError(message: string): boolean {
|
||||
return /^cannot (?:start|join) huddle: already in phase /i.test(message);
|
||||
}
|
||||
|
||||
function clamp01(value: number): number {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
|
||||
function interruptAgentSpeech(agentPubkey: string) {
|
||||
return invoke<void>("interrupt_huddle_speech", { agentPubkey });
|
||||
}
|
||||
|
||||
const HuddleContext = React.createContext<HuddleContextValue | null>(null);
|
||||
const HuddleLevelsContext = React.createContext<HuddleLevelsValue | null>(null);
|
||||
|
||||
export function HuddleProvider({
|
||||
children,
|
||||
@@ -110,7 +101,6 @@ export function HuddleProvider({
|
||||
const [mirroredAudioState, setMirroredAudioState] =
|
||||
React.useState<HuddleAudioMirrorState | null>(null);
|
||||
const [mirroredMicLevel, setMirroredMicLevel] = React.useState(0);
|
||||
const [micLevel, setMicLevel] = React.useState(0);
|
||||
const {
|
||||
getVoiceInputMode,
|
||||
pttActive,
|
||||
@@ -790,77 +780,7 @@ export function HuddleProvider({
|
||||
usePipelineHotstart(ephemeralChannelId);
|
||||
|
||||
// Mic level analyser — drives the voice activity indicator
|
||||
React.useEffect(() => {
|
||||
if (!localAudioTrack || !micConnected) {
|
||||
setMicLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = new AudioContext();
|
||||
const analyser = ctx.createAnalyser();
|
||||
analyser.fftSize = 512;
|
||||
const source = ctx.createMediaStreamSource(
|
||||
new MediaStream([localAudioTrack]),
|
||||
);
|
||||
source.connect(analyser);
|
||||
const buf = new Float32Array(analyser.fftSize);
|
||||
|
||||
let raf = 0;
|
||||
let lastUpdate = 0;
|
||||
let voiceActive = false;
|
||||
let noiseFloor = MIC_INITIAL_NOISE_FLOOR;
|
||||
let smoothedLevel = 0;
|
||||
function tick(now: number) {
|
||||
raf = requestAnimationFrame(tick);
|
||||
if (now - lastUpdate < MIC_ANALYSER_UPDATE_INTERVAL_MS) return;
|
||||
lastUpdate = now;
|
||||
analyser.getFloatTimeDomainData(buf);
|
||||
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < buf.length; i += 1) {
|
||||
sumSquares += buf[i] * buf[i];
|
||||
}
|
||||
|
||||
const rms = Math.sqrt(sumSquares / buf.length);
|
||||
const activeThreshold = Math.max(
|
||||
MIC_VOICE_GATE_ON_RMS,
|
||||
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS,
|
||||
);
|
||||
const idleThreshold = Math.max(
|
||||
MIC_VOICE_GATE_OFF_RMS,
|
||||
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS * 0.55,
|
||||
);
|
||||
voiceActive = voiceActive ? rms > idleThreshold : rms > activeThreshold;
|
||||
|
||||
const floorRate =
|
||||
rms < noiseFloor
|
||||
? 0.18
|
||||
: voiceActive
|
||||
? MIC_ACTIVE_NOISE_FLOOR_RISE
|
||||
: 0.025;
|
||||
noiseFloor += (rms - noiseFloor) * floorRate;
|
||||
|
||||
if (!voiceActive) {
|
||||
smoothedLevel = 0;
|
||||
setMicLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = clamp01(
|
||||
(rms - noiseFloor) / MIC_LEVEL_ACTIVE_RANGE_RMS,
|
||||
);
|
||||
const targetLevel = Math.max(normalized, MIC_MIN_ACTIVE_LEVEL);
|
||||
smoothedLevel += (targetLevel - smoothedLevel) * MIC_LEVEL_ATTACK;
|
||||
setMicLevel(smoothedLevel);
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
source.disconnect();
|
||||
void ctx.close();
|
||||
};
|
||||
}, [localAudioTrack, micConnected]);
|
||||
const micLevel = useMicLevelAnalyser(localAudioTrack, micConnected);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ownsAudioSession) {
|
||||
@@ -950,42 +870,87 @@ export function HuddleProvider({
|
||||
};
|
||||
}, [ownsAudioSession]);
|
||||
|
||||
// High-frequency (20-30 Hz) audio levels live in their own context so their
|
||||
// churn re-renders only the meter components, not every useHuddle consumer.
|
||||
const levelsValue = React.useMemo<HuddleLevelsValue>(
|
||||
() => ({
|
||||
micLevel: ownsAudioSession ? micLevel : mirroredMicLevel,
|
||||
activeSpeakers,
|
||||
speakerLevels,
|
||||
}),
|
||||
[
|
||||
activeSpeakers,
|
||||
micLevel,
|
||||
mirroredMicLevel,
|
||||
ownsAudioSession,
|
||||
speakerLevels,
|
||||
],
|
||||
);
|
||||
|
||||
const effectiveMicConnected = ownsAudioSession
|
||||
? micConnected
|
||||
: (mirroredAudioState?.micConnected ?? false);
|
||||
const contextValue = React.useMemo<HuddleContextValue>(
|
||||
() => ({
|
||||
localAudioTrack,
|
||||
isStarting,
|
||||
huddleError,
|
||||
clearHuddleError,
|
||||
micConnected: effectiveMicConnected,
|
||||
isMuted: effectiveIsMuted,
|
||||
toggleMute,
|
||||
interruptAgentSpeech,
|
||||
pttActive,
|
||||
voiceInputMode: effectiveVoiceInputMode,
|
||||
setVoiceInputMode,
|
||||
audioDevices,
|
||||
selectedDeviceId,
|
||||
setSelectedDeviceId,
|
||||
micGain,
|
||||
setMicGain,
|
||||
outputDevices,
|
||||
selectedOutputDevice,
|
||||
setSelectedOutputDevice,
|
||||
activeEphemeralChannelId: ephemeralChannelId,
|
||||
showHuddleInMainApp,
|
||||
viewHuddleChannel,
|
||||
startHuddle,
|
||||
joinHuddle,
|
||||
leaveHuddle,
|
||||
}),
|
||||
[
|
||||
audioDevices,
|
||||
clearHuddleError,
|
||||
effectiveIsMuted,
|
||||
effectiveMicConnected,
|
||||
effectiveVoiceInputMode,
|
||||
ephemeralChannelId,
|
||||
huddleError,
|
||||
isStarting,
|
||||
joinHuddle,
|
||||
leaveHuddle,
|
||||
localAudioTrack,
|
||||
micGain,
|
||||
outputDevices,
|
||||
pttActive,
|
||||
selectedDeviceId,
|
||||
selectedOutputDevice,
|
||||
setMicGain,
|
||||
setSelectedDeviceId,
|
||||
setSelectedOutputDevice,
|
||||
setVoiceInputMode,
|
||||
showHuddleInMainApp,
|
||||
startHuddle,
|
||||
toggleMute,
|
||||
viewHuddleChannel,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<HuddleContext.Provider
|
||||
value={{
|
||||
localAudioTrack,
|
||||
isStarting,
|
||||
huddleError,
|
||||
clearHuddleError,
|
||||
micConnected: ownsAudioSession
|
||||
? micConnected
|
||||
: (mirroredAudioState?.micConnected ?? false),
|
||||
isMuted: effectiveIsMuted,
|
||||
toggleMute,
|
||||
interruptAgentSpeech,
|
||||
micLevel: ownsAudioSession ? micLevel : mirroredMicLevel,
|
||||
pttActive,
|
||||
voiceInputMode: effectiveVoiceInputMode,
|
||||
setVoiceInputMode,
|
||||
activeSpeakers,
|
||||
speakerLevels,
|
||||
audioDevices,
|
||||
selectedDeviceId,
|
||||
setSelectedDeviceId,
|
||||
micGain,
|
||||
setMicGain,
|
||||
outputDevices,
|
||||
selectedOutputDevice,
|
||||
setSelectedOutputDevice,
|
||||
activeEphemeralChannelId: ephemeralChannelId,
|
||||
showHuddleInMainApp,
|
||||
viewHuddleChannel,
|
||||
startHuddle,
|
||||
joinHuddle,
|
||||
leaveHuddle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<HuddleContext.Provider value={contextValue}>
|
||||
<HuddleLevelsContext.Provider value={levelsValue}>
|
||||
{children}
|
||||
</HuddleLevelsContext.Provider>
|
||||
</HuddleContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -997,3 +962,16 @@ export function useHuddle(): HuddleContextValue {
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* High-frequency (20-30 Hz) mic/speaker levels. Consume only from components
|
||||
* that render audio meters; everything else should use {@link useHuddle} so it
|
||||
* is insulated from level churn.
|
||||
*/
|
||||
export function useHuddleLevels(): HuddleLevelsValue {
|
||||
const ctx = React.useContext(HuddleLevelsContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useHuddleLevels must be used within a HuddleProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { AudioInputDevice } from "./lib/useAudioDevices";
|
||||
import type { VoiceInputMode } from "./lib/useHuddlePttState";
|
||||
|
||||
/**
|
||||
* High-frequency audio-level fields, split from {@link HuddleContextValue} so
|
||||
* their 20-30 Hz updates only re-render the meter components that consume
|
||||
* them — not every `useHuddle()` consumer across the app.
|
||||
*/
|
||||
export interface HuddleLevelsValue {
|
||||
micLevel: number;
|
||||
activeSpeakers: string[];
|
||||
speakerLevels: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface HuddleContextValue {
|
||||
localAudioTrack: MediaStreamTrack | null;
|
||||
isStarting: boolean;
|
||||
@@ -11,12 +22,9 @@ export interface HuddleContextValue {
|
||||
toggleMute: () => void;
|
||||
/** Interrupt this agent only if it still owns the active utterance. */
|
||||
interruptAgentSpeech: (agentPubkey: string) => Promise<void>;
|
||||
micLevel: number;
|
||||
pttActive: boolean;
|
||||
voiceInputMode: VoiceInputMode;
|
||||
setVoiceInputMode: (mode: VoiceInputMode) => Promise<void>;
|
||||
activeSpeakers: string[];
|
||||
speakerLevels: Record<string, number>;
|
||||
audioDevices: AudioInputDevice[];
|
||||
selectedDeviceId: string;
|
||||
setSelectedDeviceId: (id: string) => void;
|
||||
|
||||
@@ -27,7 +27,7 @@ import { Button } from "@/shared/ui/button";
|
||||
import { useEmojiBurst } from "@/shared/ui/EmojiBurstProvider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { useHuddle } from "../HuddleContext";
|
||||
import { useHuddle, useHuddleLevels } from "../HuddleContext";
|
||||
import { AddAgentDialog, type AgentAddResult } from "./AddAgentDialog";
|
||||
import type { HuddleAgentVoiceSettings } from "./AgentVoiceMenu";
|
||||
import { MicControls, SpeakerControls } from "./MicControls";
|
||||
@@ -157,11 +157,8 @@ export function HuddleBar({
|
||||
micConnected,
|
||||
isMuted,
|
||||
toggleMute,
|
||||
micLevel,
|
||||
voiceInputMode,
|
||||
setVoiceInputMode,
|
||||
activeSpeakers,
|
||||
speakerLevels,
|
||||
huddleError,
|
||||
clearHuddleError,
|
||||
audioDevices,
|
||||
@@ -173,6 +170,7 @@ export function HuddleBar({
|
||||
selectedOutputDevice,
|
||||
setSelectedOutputDevice,
|
||||
} = useHuddle();
|
||||
const { activeSpeakers, micLevel, speakerLevels } = useHuddleLevels();
|
||||
const customEmoji = useCustomEmoji();
|
||||
const identityQuery = useIdentityQuery();
|
||||
const profileQuery = useProfileQuery();
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as React from "react";
|
||||
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useHuddle } from "../HuddleContext";
|
||||
import { useHuddle, useHuddleLevels } from "../HuddleContext";
|
||||
import { MicControls } from "./MicControls";
|
||||
|
||||
type HuddleProfileState = {
|
||||
@@ -42,7 +42,6 @@ export function HuddleProfileControl({
|
||||
leaveHuddle,
|
||||
micConnected,
|
||||
micGain,
|
||||
micLevel,
|
||||
selectedDeviceId,
|
||||
setMicGain,
|
||||
setSelectedDeviceId,
|
||||
@@ -50,6 +49,7 @@ export function HuddleProfileControl({
|
||||
toggleMute,
|
||||
voiceInputMode,
|
||||
} = useHuddle();
|
||||
const { micLevel } = useHuddleLevels();
|
||||
const [isLeaving, setIsLeaving] = React.useState(false);
|
||||
const [state, setState] = React.useState<HuddleProfileState | null>(null);
|
||||
const lastHuddleChannelIdRef = React.useRef<string | null>(null);
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
|
||||
import { useProfileQuery, useSelfProfileCache } from "@/features/profile/hooks";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { useHuddle } from "../HuddleContext";
|
||||
import { useHuddle, useHuddleLevels } from "../HuddleContext";
|
||||
import type { HuddleAgentVoiceSettings } from "./AgentVoiceMenu";
|
||||
import { HuddleParticipantsControl } from "./ParticipantList";
|
||||
|
||||
@@ -28,14 +28,8 @@ function isVisible(state: HuddleRosterState | null) {
|
||||
|
||||
/** Larger, persistent roster for the companion huddle room window. */
|
||||
export function HuddleRoomHeader() {
|
||||
const {
|
||||
activeSpeakers,
|
||||
interruptAgentSpeech,
|
||||
isMuted,
|
||||
micConnected,
|
||||
micLevel,
|
||||
speakerLevels,
|
||||
} = useHuddle();
|
||||
const { interruptAgentSpeech, isMuted, micConnected } = useHuddle();
|
||||
const { activeSpeakers, micLevel, speakerLevels } = useHuddleLevels();
|
||||
const identityQuery = useIdentityQuery();
|
||||
const profileQuery = useProfileQuery();
|
||||
const selfProfileCache = useSelfProfileCache();
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export { HuddleProvider, useHuddle } from "./HuddleContext";
|
||||
export {
|
||||
HuddleProvider,
|
||||
useHuddle,
|
||||
useHuddleLevels,
|
||||
} from "./HuddleContext";
|
||||
export { setupAudioWorklet } from "./lib/audioWorklet";
|
||||
export { HuddleBar } from "./components/HuddleBar";
|
||||
export { HuddleProfileControl } from "./components/HuddleProfileControl";
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MIC_ANALYSER_UPDATE_INTERVAL_MS = 33;
|
||||
const MIC_INITIAL_NOISE_FLOOR = 0.01;
|
||||
const MIC_VOICE_GATE_ON_RMS = 0.018;
|
||||
const MIC_VOICE_GATE_OFF_RMS = 0.012;
|
||||
const MIC_VOICE_GATE_MARGIN_RMS = 0.012;
|
||||
const MIC_LEVEL_ACTIVE_RANGE_RMS = 0.11;
|
||||
const MIC_MIN_ACTIVE_LEVEL = 0.18;
|
||||
const MIC_LEVEL_ATTACK = 0.58;
|
||||
const MIC_ACTIVE_NOISE_FLOOR_RISE = 0.006;
|
||||
|
||||
function clamp01(value: number): number {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mic level analyser — drives the voice activity indicator. Emits a smoothed
|
||||
* 0..1 level at up to ~30 Hz while the local track is live; 0 when idle.
|
||||
*/
|
||||
export function useMicLevelAnalyser(
|
||||
localAudioTrack: MediaStreamTrack | null,
|
||||
micConnected: boolean,
|
||||
): number {
|
||||
const [micLevel, setMicLevel] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!localAudioTrack || !micConnected) {
|
||||
setMicLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = new AudioContext();
|
||||
const analyser = ctx.createAnalyser();
|
||||
analyser.fftSize = 512;
|
||||
const source = ctx.createMediaStreamSource(
|
||||
new MediaStream([localAudioTrack]),
|
||||
);
|
||||
source.connect(analyser);
|
||||
const buf = new Float32Array(analyser.fftSize);
|
||||
|
||||
let raf = 0;
|
||||
let lastUpdate = 0;
|
||||
let voiceActive = false;
|
||||
let noiseFloor = MIC_INITIAL_NOISE_FLOOR;
|
||||
let smoothedLevel = 0;
|
||||
function tick(now: number) {
|
||||
raf = requestAnimationFrame(tick);
|
||||
if (now - lastUpdate < MIC_ANALYSER_UPDATE_INTERVAL_MS) return;
|
||||
lastUpdate = now;
|
||||
analyser.getFloatTimeDomainData(buf);
|
||||
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < buf.length; i += 1) {
|
||||
sumSquares += buf[i] * buf[i];
|
||||
}
|
||||
|
||||
const rms = Math.sqrt(sumSquares / buf.length);
|
||||
const activeThreshold = Math.max(
|
||||
MIC_VOICE_GATE_ON_RMS,
|
||||
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS,
|
||||
);
|
||||
const idleThreshold = Math.max(
|
||||
MIC_VOICE_GATE_OFF_RMS,
|
||||
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS * 0.55,
|
||||
);
|
||||
voiceActive = voiceActive ? rms > idleThreshold : rms > activeThreshold;
|
||||
|
||||
const floorRate =
|
||||
rms < noiseFloor
|
||||
? 0.18
|
||||
: voiceActive
|
||||
? MIC_ACTIVE_NOISE_FLOOR_RISE
|
||||
: 0.025;
|
||||
noiseFloor += (rms - noiseFloor) * floorRate;
|
||||
|
||||
if (!voiceActive) {
|
||||
smoothedLevel = 0;
|
||||
setMicLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = clamp01(
|
||||
(rms - noiseFloor) / MIC_LEVEL_ACTIVE_RANGE_RMS,
|
||||
);
|
||||
const targetLevel = Math.max(normalized, MIC_MIN_ACTIVE_LEVEL);
|
||||
smoothedLevel += (targetLevel - smoothedLevel) * MIC_LEVEL_ATTACK;
|
||||
setMicLevel(smoothedLevel);
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
source.disconnect();
|
||||
void ctx.close();
|
||||
};
|
||||
}, [localAudioTrack, micConnected]);
|
||||
|
||||
return micLevel;
|
||||
}
|
||||
Reference in New Issue
Block a user