# Voice

Added in 0.2. Earshot makes voices sound like they're in your world. Use any `RTCPeerConnection` you already have, or the [voice rooms](#voice-rooms) below. We run no servers for you.

## Voice chat {#voice-chat}

```ts
import { createVoiceMic } from "@debuffjp/earshot";

const mic = createVoiceMic({ canUseVoice: () => settings.voiceAllowed });
const { ok, error } = await mic.start(); // Call from a tap or key press.
const stream = mic.stream;
if (ok && stream) stream.getTracks().forEach((track) => connection.addTrack(track, stream));

connection.ontrack = (event) => {
  const voice = ear.voice(event.streams[0], { pos: () => friend });
  // voice.state, voice.speaking and voice.levelDb are there for your UI.
};
```

A voice goes through the same walls, rooms and pan as any other source, and needs `ear.update()` in the same way. `ear.voice()` needs an `AudioContext`, not an `OfflineAudioContext`.

| On a voice | Meaning |
| --- | --- |
| `voice.muted` | Mutes this player locally |
| `voice.levelDb` | Their level before distance and walls, in dB |
| `voice.speaking` | For a talking indicator. Ignores distance and walls, so check `state` if range matters |
| `voice.state`, `voice.mix`, `voice.reverb` | The same as on any [source](./sound#sources) |
| `voice.remove()` | Disconnects this voice |

`speaking` turns on above `speakingDb`, an option of `ear.voice()` that defaults to -50.

## Mic controls {#mic-controls}

The mic is off until the game asks, and it records nothing.

```ts
mic.muted = true;
mic.pushToTalk = true;
mic.talk(true); // Hold to talk, while pushToTalk is on.
mic.talk(false);
mic.live; // True when audio is being sent. Show players this.
mic.stop();
```

`canUseVoice` is required, and the mic won't open while it returns false: use it for consent checks and flags such as a portal's `disableChat`. `start()` then returns `{ ok: false, error: "not-allowed" }`. It's checked when you call `start()`, so if a player's permission goes away later, call `mic.stop()` yourself.

`start()` never rejects. If the browser refuses, `error` is the browser's error name.

| Option | Meaning | Default |
| --- | --- | --- |
| `canUseVoice()` | Your check that this player may use voice. Required | |
| `echoCancellation` | `true`, `false`, `"all"` or `"remote-only"`. `"all"` also asks the browser to cancel other audio where it can | `true` |
| `pushToTalk` | While on, the mic only sends during `talk(true)` | `false` |

`mic.applied` says what the browser really applied. Noise suppression and auto gain are always requested.

## Voice rooms {#voice-rooms}

If you don't have a connection already, `joinVoiceRoom()` connects players using a small Cloudflare Worker that [you deploy to your own account](./self-hosting). We don't host it. It gives you invite-only rooms, a player cap and short-lived TURN credentials, and its `canJoin` function is where your login or ban checks go.

```ts
import { createRoomCode, joinVoiceRoom, type Point } from "@debuffjp/earshot";

const code = createRoomCode(); // Share it with friends.
const joined = await joinVoiceRoom({
  url: "wss://earshot-voice.you.workers.dev",
  code,
  mic,
  listener: ear,
  ipPolicy: "relay", // Or "direct": see below.
  pos: (peer) => players.get(peer) ?? spawn,
  onMessage: (peer, position) => players.set(peer, position as Point),
});

if (joined.ok) setInterval(() => joined.room.send(player), 66);
```

`mic` must have started before you join. `pos` is called for every player, including before their first position arrives, so give it a fallback. Anyone with the code can join, so share it only with friends.

`joinVoiceRoom()` never rejects. `ok: true` means you're in the room, while connections to each player may still be forming.

| On the options | Meaning |
| --- | --- |
| `onPeer(peer, voice)` | A player's voice arrived |
| `onLeave(peer)` | A player left. Also called for everyone when you leave |
| `onMessage(peer, message)` | A small message from `room.send`. It can arrive late or not at all |

| On the room | Meaning |
| --- | --- |
| `room.id` | This player's id |
| `room.peers` | Each player's `Voice`, by id |
| `room.connection(peer)` | Connection state for your UI: `"connecting"`, `"connected"`, `"failed"` and so on |
| `room.send(message)` | Sends JSON to every connected player, directly rather than through your Worker |
| `room.leave()` | Leaves the room. The mic stays open until `mic.stop()` |

## ipPolicy {#ippolicy}

`ipPolicy` is required because it's a privacy decision. `"relay"` hides IP addresses from other players and fails rather than falling back, but all voice is billed as TURN traffic. `"direct"` costs less, and players can see each other's IP addresses.

[Self-hosting](./self-hosting#costs) has the cost table.

## Connections and errors {#connections-and-errors}

Everyone connects to everyone, so upload grows with each player. Rooms stop sending to players well out of range, which means further than 1.25 times `silentRadius`.

Errors come back as `joined.error`:

| Error | Meaning |
| --- | --- |
| `room-full` | The room is at the Worker's `MAX_PLAYERS` |
| `no-turn-server` | `ipPolicy` is `"relay"` and the Worker offered no TURN server |
| `mic-not-started` | `mic.start()` hasn't succeeded |
| `connection-failed` | The Worker couldn't be reached or refused the connection |

Dropped connections don't rejoin automatically, and `room.connection(peer)` lets you show that.

## Your duties {#your-duties}

**These controls don't make your game compliant.** Live voice between players can bring legal duties, especially where children may play: COPPA in the US and the Online Safety Act in the UK, for example. A library in the browser can't verify ages, enforce bans, moderate or take reports, so those are yours to plan for. Keep voice off by default and limit it to friends. This isn't legal advice.

## Tested on {#tested-on}

Tested with a PC and an iPhone on one network, and on relay between Chromium and Firefox on desktop. Larger groups are untested.

[Earshot 0.2: voice chat that hears your walls](/blog/earshot-sound-through-walls) introduces these features.