feat: Implement microphone monitoring feature; enhance text overflow handling and line height across components
This commit is contained in:
@@ -454,7 +454,11 @@ export function Audio({ requestClose }: AudioProps) {
|
||||
const [micTestState, setMicTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle');
|
||||
const [micLevel, setMicLevel] = useState(0);
|
||||
const [speakerTestState, setSpeakerTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle');
|
||||
const [micMonitoring, setMicMonitoring] = useState(false);
|
||||
const micAnimFrameRef = useRef<number | null>(null);
|
||||
const micMonitorContextRef = useRef<AudioContext | null>(null);
|
||||
const micMonitorStreamRef = useRef<MediaStream | null>(null);
|
||||
const micMonitorGainRef = useRef<GainNode | null>(null);
|
||||
const [showRemoteCursor, setShowRemoteCursor] = useSetting(settingsAtom, 'showRemoteCursor');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -627,6 +631,101 @@ export function Audio({ requestClose }: AudioProps) {
|
||||
}
|
||||
}, [speakerTestState]);
|
||||
|
||||
const handleMicMonitoringToggle = useCallback(async () => {
|
||||
if (micMonitoring) {
|
||||
// Stop monitoring
|
||||
if (micAnimFrameRef.current !== null) {
|
||||
cancelAnimationFrame(micAnimFrameRef.current);
|
||||
micAnimFrameRef.current = null;
|
||||
}
|
||||
if (micMonitorStreamRef.current) {
|
||||
micMonitorStreamRef.current.getTracks().forEach((track) => track.stop());
|
||||
micMonitorStreamRef.current = null;
|
||||
}
|
||||
if (micMonitorContextRef.current) {
|
||||
micMonitorContextRef.current.close();
|
||||
micMonitorContextRef.current = null;
|
||||
}
|
||||
micMonitorGainRef.current = null;
|
||||
setMicLevel(0);
|
||||
setMicMonitoring(false);
|
||||
} else {
|
||||
// Start monitoring
|
||||
try {
|
||||
const constraints: MediaStreamConstraints = {
|
||||
audio: settings.microphoneId
|
||||
? {
|
||||
deviceId: { exact: settings.microphoneId },
|
||||
noiseSuppression: settings.noiseSuppression,
|
||||
echoCancellation: settings.echoCancellation,
|
||||
autoGainControl: settings.autoGainControl,
|
||||
}
|
||||
: {
|
||||
noiseSuppression: settings.noiseSuppression,
|
||||
echoCancellation: settings.echoCancellation,
|
||||
autoGainControl: settings.autoGainControl,
|
||||
},
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
// Connect microphone to analyser for level monitoring
|
||||
microphone.connect(analyser);
|
||||
|
||||
// Connect microphone to speakers for playback
|
||||
microphone.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
gainNode.gain.value = 1.0; // Full volume playback
|
||||
|
||||
micMonitorContextRef.current = audioContext;
|
||||
micMonitorStreamRef.current = stream;
|
||||
micMonitorGainRef.current = gainNode;
|
||||
|
||||
const tick = () => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
|
||||
setMicLevel(Math.min(100, Math.round((avg / 255) * 100 * 3)));
|
||||
micAnimFrameRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
micAnimFrameRef.current = requestAnimationFrame(tick);
|
||||
|
||||
setMicMonitoring(true);
|
||||
} catch (e) {
|
||||
console.error('Microphone monitoring failed:', e);
|
||||
}
|
||||
}
|
||||
}, [micMonitoring, settings.microphoneId, settings.noiseSuppression, settings.echoCancellation, settings.autoGainControl]);
|
||||
|
||||
// Cleanup monitoring on unmount or when settings change
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (micAnimFrameRef.current !== null) {
|
||||
cancelAnimationFrame(micAnimFrameRef.current);
|
||||
}
|
||||
if (micMonitorStreamRef.current) {
|
||||
micMonitorStreamRef.current.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
if (micMonitorContextRef.current) {
|
||||
micMonitorContextRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Restart monitoring when mic settings change
|
||||
useEffect(() => {
|
||||
if (micMonitoring) {
|
||||
handleMicMonitoringToggle(); // Stop
|
||||
setTimeout(() => handleMicMonitoringToggle(), 100); // Restart
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [settings.microphoneId, settings.noiseSuppression, settings.echoCancellation, settings.autoGainControl]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
@@ -686,21 +785,35 @@ export function Audio({ requestClose }: AudioProps) {
|
||||
onSelect={handleMicrophoneSelect}
|
||||
/>
|
||||
<Box direction="Column" gap="100" alignItems="End">
|
||||
<Button
|
||||
size="300"
|
||||
variant={micTestState === 'error' ? 'Critical' : micTestState === 'done' ? 'Success' : 'Secondary'}
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={handleTestMicrophone}
|
||||
disabled={micTestState === 'testing'}
|
||||
>
|
||||
<Text size="T300">
|
||||
{micTestState === 'testing' ? 'Testing...' : micTestState === 'done' ? 'Done ✓' : micTestState === 'error' ? 'Failed' : 'Test'}
|
||||
</Text>
|
||||
</Button>
|
||||
{micTestState === 'testing' && (
|
||||
<Box style={{ width: toRem(80), height: toRem(6), background: color.Surface.Container, borderRadius: toRem(3), overflow: 'hidden' }}>
|
||||
<Box style={{ width: `${micLevel}%`, height: '100%', background: color.Primary.Main, transition: 'width 0.05s ease' }} />
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Button
|
||||
size="300"
|
||||
variant={micTestState === 'error' ? 'Critical' : micTestState === 'done' ? 'Success' : 'Secondary'}
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={handleTestMicrophone}
|
||||
disabled={micTestState === 'testing' || micMonitoring}
|
||||
>
|
||||
<Text size="T300">
|
||||
{micTestState === 'testing' ? 'Testing...' : micTestState === 'done' ? 'Done ✓' : micTestState === 'error' ? 'Failed' : 'Test'}
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300"
|
||||
variant={micMonitoring ? 'Primary' : 'Secondary'}
|
||||
fill={micMonitoring ? 'Solid' : 'Soft'}
|
||||
radii="300"
|
||||
onClick={handleMicMonitoringToggle}
|
||||
disabled={micTestState === 'testing'}
|
||||
>
|
||||
<Text size="T300">
|
||||
{micMonitoring ? 'Stop' : 'Monitor'}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
{(micTestState === 'testing' || micMonitoring) && (
|
||||
<Box style={{ width: toRem(160), height: toRem(6), background: color.Surface.Container, borderRadius: toRem(3), overflow: 'hidden' }}>
|
||||
<Box style={{ width: `${micLevel}%`, height: '100%', background: micMonitoring ? color.Success.Main : color.Primary.Main, transition: 'width 0.05s ease' }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user