Audio device testPro
The audio check before a call: pick a microphone and watch it pass on your voice, pick a speaker and hear a chime through it. Too quiet, no sound and too loud each say what to change. The setup flow version of the free voice controls.
On this page
Installation
Usage
import { AudioDeviceTest } from "@/components/ui/chatcn/pro/audio-device-test"
<AudioDeviceTest
microphones={devices.microphones}
microphone={micId}
onMicrophoneChange={setMicId}
speakers={devices.speakers}
speaker={speakerId}
onSpeakerChange={setSpeakerId}
inputLevel={meter.getLevel}
onContinue={joinCall}
/>Devices and the level are plain props, in the shape the free useChatcnVoiceDevices, useChatcnMicrophone and useChatcnAudioLevel hooks return, so you can feed it real devices or a simulation. The microphone passes once speech stays above the mark for about a quarter of a second, so a click or a cough does not count. After a few seconds of a weak signal it says too quiet, of a flat line no sound (usually a mute switch or a system setting), and sustained full scale is too loud.
Play test tone plays a quiet two note chime through the chosen output with AudioContext.setSinkId, falling back to an audio element's setSinkId, then asks whether you heard it. Where the browser cannot pick outputs the speaker row says so and plays through the system output. The audio context starts inside the click, as browsers require. Pass onTestTone to play your own sound.
Examples
Every state
Your real devices
Props
| Prop | Type | Notes |
|---|---|---|
microphones, speakers | ChatcnAudioDevice[] | From useChatcnVoiceDevices |
microphone, onMicrophoneChange | string, (id) => void | A chosen device that disappears is named as disconnected |
speaker, onSpeakerChange | string, (id) => void | A new speaker resets the speaker check |
inputLevel | number | () => number | 0 to 1. A getter drives the meter without re-rendering |
micStatus | "needs-permission" | "requesting" | "active" | "error" | Default active. The first two show the inline permission gate |
micError | "denied" | "system-blocked" | "not-found" | "in-use" | "unsupported" | Shown with its fix when micStatus is error |
onRequestMicrophone | () => void | From the gate's Allow and Try again |
canPickSpeaker | boolean | Detected when undefined |
check | "waiting" | "heard" | "quiet" | "silent" | "clipping" | Force the verdict. Measured from inputLevel otherwise |
speakerCheck, defaultSpeakerCheck, onSpeakerCheckChange | "idle" | "playing" | "asked" | "yes" | "no" | Controlled or uncontrolled |
onTestTone | (speakerId) => Promise | undefined | Play your own sound instead of the chime |
onContinue, continueLabel | () => void, string | Default "Join call". Enabled once the microphone is heard |
requireCheck | boolean | Default true. Off enables continue at once |
onSkip, skipLabel | () => void, string | Default "Skip" |
disabled | boolean | Disables every control and stops the meter |
title, description | ReactNode | Heading and one line under it. null hides the line |
Keyboard
| Key | Action |
|---|---|
Tab | Moves through the selects, Play test tone, Yes and No, then the footer |
Enter, Space | Opens a select or activates the focused button |
Arrow keys | Move through devices in an open select |
Accessibility
- The selects take their names from the visible Microphone and Speaker labels.
- The meter is hidden from assistive technology; the verdict is announced instead, once per change, through a polite live region.
- Verdicts never rely on color: a check or warning glyph and a word sit beside the green or amber.
- Join call stays disabled until the check passes, with the reason written next to it.
- Every control grows to 44 px on coarse pointers.
Motion
- The meter is written to the DOM each frame as one clip on the lit layer, with a peak cell that holds for 700 ms and then falls. It keeps moving under reduced motion: it is the reading, not decoration.
- Verdicts and the play button label swap with a 150 ms crossfade; the speaker question rises in over 180 ms.