# Mic Selector

Mic Selector [#mic-selector]

**Mic Selector** is a microphone input-device picker with permission handling and device enumeration. It is fully offline — it uses only the browser device APIs (`getUserMedia` for the one-time permission prompt, `enumerateDevices` for the list, and a `devicechange` listener to stay current). Selecting a device emits its `deviceId`, which flows straight into [`useVoiceRecorder({ deviceId })`](https://localmode.dev/docs/react) — the recorder requests `{ deviceId: { exact } }`, so the selected microphone is actually used (recording errors rather than silently falling back when the device is unavailable).

Pairs with [`useVoiceRecorder` / `useLiveTranscribe`](https://localmode.dev/docs/react), `VoiceButton`, and `VoiceOrb`.

**When to use it:** let users choose which microphone feeds a voice agent or transcription pipeline.

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';
import { MicSelector } from '@/components/mic-selector';

/**
 * Demo for {@link MicSelector}. Renders the real device picker — clicking
 * "Allow microphone" triggers a genuine browser permission prompt and lists the
 * actual audio inputs (labels appear after permission). The selected id is shown
 * below; the real app routes it into a `getUserMedia({ audio: { deviceId } })`
 * constraint for `VoiceButton` / `VoiceOrb`.
 */
export default function MicSelectorDemo() {
  const [deviceId, setDeviceId] = useState('');

  return (
    <div className="flex flex-col gap-3">
      <MicSelector value={deviceId} onValueChange={setDeviceId} />
      <code className="text-xs text-muted-foreground">
        selected: {deviceId || '(none)'}
      </code>
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/audio/mic-selector
```

Data source & dependencies [#data-source--dependencies]

**Data source:** enumerates devices via the browser `MediaDevices` API and emits the chosen `deviceId` — works with any backend. Recommended consumer: feed it into `useVoiceRecorder({ deviceId })` (pairs with `useLiveTranscribe`) from `@localmode/react` (on-device, optional).

* `clsx` + `tailwind-merge` — via the shared `cn()` util
* No external dependency — browser `MediaDevices` API only

Files installed [#files-installed]

* `mic-selector.tsx` — the component
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**MicSelector**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | Selected device id (controlled). |
| `onValueChange` | `function` | — | Fired with the chosen device id. |
| `label` | `string` | `"Microphone"` | Accessible label. |

Examples [#examples]

Route the chosen device into a recorder [#route-the-chosen-device-into-a-recorder]

```tsx
import { MicSelector } from '@/components/mic-selector';
import { useVoiceRecorder } from '@localmode/react';

export function Capture() {
  const [deviceId, setDeviceId] = useState('');
  const recorder = useVoiceRecorder(deviceId ? { deviceId } : undefined);

  return (
    <div className="flex items-center gap-2">
      <MicSelector value={deviceId} onValueChange={setDeviceId} />
      <button onClick={recorder.startRecording}>Start</button>
    </div>
  );
}
```

`useVoiceRecorder` forwards `deviceId` to `getUserMedia` as `{ deviceId: { exact: deviceId } }` and surfaces a failure on `recorder.error` instead of silently recording from the wrong microphone. While recording, `recorder.stream` exposes the live `MediaStream` and `recorder.getVolume()` returns the RMS input level for meters.

Customization [#customization]

Device labels are empty until permission is granted — the component shows an "Allow microphone" button until then, and surfaces a "Permission denied" message on rejection. It releases the mic immediately after reading labels (it only needs permission, not a live stream). The `devicechange` listener keeps the list current when devices are plugged/unplugged.