import { useEffect, useRef, useState } from 'react'; import { useVoiceStore } from '../../../stores/voiceStore'; import { AudioManager } from '../../../audio/AudioManager'; import { useAudioDevices } from '../../../hooks/useAudioDevices'; export function AudioInputSection() { const inputDeviceId = useVoiceStore((s) => s.inputDeviceId); const setInputDevice = useVoiceStore((s) => s.setInputDevice); const inputVolume = useVoiceStore((s) => s.inputVolume); const setInputVolume = useVoiceStore((s) => s.setInputVolume); const { permState, inputs, inputLabels, requestPermission } = useAudioDevices(); const [listOpen, setListOpen] = useState(false); const [micLevel, setMicLevel] = useState(0); const [activeUpstreamId, setActiveUpstreamId] = useState(null); const dropdownRef = useRef(null); const animFrameRef = useRef(0); // Click-outside-to-close. useEffect(() => { if (!listOpen) return; const onMouseDown = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setListOpen(false); } }; document.addEventListener('mousedown', onMouseDown); return () => document.removeEventListener('mousedown', onMouseDown); }, [listOpen]); // Live mic-level meter. Reuses AudioManager's analyser node, which is part // of the canonical pipeline — no extra getUserMedia required if the user is // already in voice OR the AudioContext is active. useEffect(() => { if (permState !== 'granted') return; let stopped = false; const am = AudioManager.getInstance(); const ctx = am.getContext(); if (!ctx) return; // nothing to measure until the user joins voice or hits Test const analyser = am.getAnalyserNode(); analyser.fftSize = 256; const data = new Uint8Array(analyser.frequencyBinCount); const tick = () => { if (stopped) return; analyser.getByteFrequencyData(data); const avg = data.reduce((a, b) => a + b, 0) / data.length; setMicLevel(Math.min(avg / 128, 1)); animFrameRef.current = requestAnimationFrame(tick); }; tick(); return () => { stopped = true; if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current); }; }, [permState]); // Track the resolved upstream deviceId for the "System Default · X" hint. useEffect(() => { if (permState !== 'granted') return; const am = AudioManager.getInstance(); const id = am.getCurrentInputDeviceId(); setActiveUpstreamId(id === 'default' ? null : id); }, [permState, inputDeviceId]); if (permState === 'unknown') { return (
Checking microphone access…
); } if (permState === 'denied') { return (
⚠ Microphone access denied
Grant microphone permission to choose an input device.
); } if (permState === 'prompt') { return (
Microphone permission needed to list and choose an input device.
); } // permState === 'granted' const selectedLabel = inputDeviceId === 'default' ? 'System Default' : inputLabels.get(inputDeviceId) ?? 'System Default'; const resolvedHint = inputDeviceId === 'default' && activeUpstreamId ? inputLabels.get(activeUpstreamId) : null; const handleSelect = (id: string) => { setInputDevice(id); AudioManager.getInstance().setInputDevice(id).catch(() => {}); setListOpen(false); }; const micBars = 20; const activeBars = Math.round(micLevel * micBars * (inputVolume / 100)); return (
{listOpen && (
handleSelect('default')} /> {inputs.filter(d => d.deviceId !== 'default').map((d) => ( handleSelect(d.deviceId)} /> ))}
)}
{resolvedHint && (
Currently using: {resolvedHint}
)} {inputs.length === 0 && (
No microphones detected.
)}
Input Volume
{inputVolume}%
setInputVolume(Number(e.target.value))} className="w-full h-1.5 rounded-full appearance-none cursor-pointer bg-surface-base [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md" style={{ background: `linear-gradient(to right, rgb(var(--accent-primary)) 0%, rgb(var(--accent-primary)) ${inputVolume / 2}%, rgb(var(--interactive-muted)) ${inputVolume / 2}%, rgb(var(--interactive-muted)) 100%)`, }} />
{Array.from({ length: micBars }).map((_, i) => (
))}
The level meter is live whenever an audio session is active. Join a voice channel to test mic input.
); } function SectionShell({ title, children }: { title: string; children: React.ReactNode }) { return (
{title}
{children}
); } interface DropdownItemProps { label: string; active: boolean; onClick: () => void; } function DropdownItem({ label, active, onClick }: DropdownItemProps) { return ( ); }