# Earshot 0.2: voice chat that hears your walls

Earshot makes sound in a 2D web game follow the map: a friend behind a wall sounds muffled, a cave echoes, and a shout wakes the monster only if it carries that far. It has no runtime dependencies and works with any engine that can give it a Web Audio context. 0.2 adds voice chat, with rooms joined by invite code on a small Worker you deploy to your own Cloudflare account.

```bash
npm i @debuffjp/earshot
```

| Where the sound is | What the player hears | Default filter and gain |
| --- | --- | --- |
| Clear line, under 2 tiles | Full volume | None |
| Behind a thin wall | Muffled, words survive | 2000 Hz, -6 dB |
| Behind a solid wall | Duller and quieter | 1200 Hz, -12 dB |
| Behind two or more walls | Mostly tone | 700 Hz, -18 dB |
| 12 tiles or more | Silence | Sound is off |

These defaults were tuned by ear. We [measured what they do to the signal](/research/what-a-wall-does-to-sound) too.

## Give it your map

You tell Earshot where the listener is and what stands between two points, then call `ear.update()` from your game loop. Everything else happens in the audio graph.

```ts
import { createListener, wallsOnGrid } from "@debuffjp/earshot";

const ear = createListener(ctx, {
  tile: 16,
  pos: () => player,
  world: {
    wallsBetween: (a, b) => wallsOnGrid(tiles, a, b),
    reverbAt: (p) => (p.y > 20 ? "cave" : null),
  },
});

const radio = ear.source(radioNode, { pos: () => radioPosition });

// In your game loop. 10 to 20 times a second is enough.
ear.update();
```

Any `AudioNode` can be a source, so sound effects, music from a jukebox in the corner and player voices all follow the same rules.

## Your voice as a controller

`createLoudness` turns the mic into levels from 0 to 3, measured against the room's own noise. Audio goes to an analyser and no further: it isn't recorded, sent or played back. Combine a level with the walls and you get a stealth rule in three lines:

```ts
const walls = wallsOnGrid(tiles, player, monster);
const mix = computeMix(player, monster, walls, DEFAULT_PARAMS, DEFAULT_MATERIALS);
if (loud.level >= 2 && heardDb(loud.level, mix) >= 6) monster.wake();
```

Every level can also come from a key through `press()`, so players without a mic get the same game.

## Voice chat in your world

New in 0.2, `ear.voice()` puts a remote player's stream through those same walls and rooms. If you already have an `RTCPeerConnection`, that's all you need. If you don't, `joinVoiceRoom()` connects players for you:

```ts
const joined = await joinVoiceRoom({
  url: "wss://earshot-voice.you.workers.dev",
  code: createRoomCode(),
  mic,
  listener: ear,
  ipPolicy: "relay",
  pos: (peer) => players.get(peer) ?? player, // Until their first position arrives.
  onMessage: (peer, position) => players.set(peer, position),
});
```

Two decisions stay with you, on purpose. The mic can't open until your `canUseVoice` function allows it, which is where consent and flags such as a portal's `disableChat` belong. And `ipPolicy` has no default: `"relay"` hides IP addresses from other players and bills voice as TURN traffic, while `"direct"` costs less and lets players see each other's addresses. Live voice between players can bring legal duties, especially where children may play, so read [the voice chat docs](/earshot/#voice-chat) before you ship it.

## What the server costs

We don't host anything. The [Worker template](https://github.com/debuffjp/earshot/tree/main/templates/worker) runs on your account. It passes connection messages between players, enforces the player cap and issues TURN credentials, and it has a `canJoin` function for your login or ban checks. Anyone with a room code can join until you add those checks.

Relay is the expensive setting, so here's our calculation for its worst case: everyone talks constantly, at 32 kbps Opus with packet overhead, which is about 24 MB per stream per hour. Cloudflare bills TURN on outgoing traffic at $0.05 per GB after 1,000 GB free each month ([pricing](https://developers.cloudflare.com/realtime/sfu/platform/pricing/)). Everyone connects to everyone, so a room of n players carries n x (n - 1) streams: 12 for 4 players, 56 for 8.

| Room, all on relay | GB per hour | Cost per hour after the free tier | Room-hours in the free tier |
| --- | --- | --- | --- |
| 4 players | 0.29 | $0.014 | About 3,470 |
| 8 players | 1.34 | $0.067 | About 740 |

Real rooms cost less, because rooms stop sending to players well out of range and push-to-talk sends silence while the key is up, which is smaller but still traffic. The table covers TURN only: Workers and Durable Objects usage for the room server is billed separately.

Start with the [Earshot docs](/earshot/), then put one wall between your player and a radio.