# Earshot 0.2：壁を聞き分けるボイスチャット {#earshot-0-2-voice-chat-that-hears-your-walls}

Earshot は、2D Web ゲームのサウンドをマップに合わせます。壁の向こうの仲間の声はこもり、洞窟では響き、叫び声は届く距離にいるモンスターだけを起こします。ランタイム依存はなく、Web Audio のコンテキストを渡せるエンジンなら、どれでも使えます。0.2 ではボイスチャットが加わりました。招待コードで入るルームは、自分の Cloudflare アカウントにデプロイする小さな Worker で動きます。

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

| 音の位置 | プレイヤーに聞こえる音 | デフォルトのフィルターとゲイン |
| --- | --- | --- |
| 遮るものなし、2 タイル未満 | 最大音量 | なし |
| 薄い壁の向こう | こもるが、言葉は聞き取れる | 2000 Hz、-6 dB |
| 厚い壁の向こう | さらにこもり、小さくなる | 1200 Hz、-12 dB |
| 2 枚以上の壁の向こう | ほとんど音色だけ | 700 Hz、-18 dB |
| 12 タイル以上 | 無音 | 音はオフ |

これらのデフォルト値は耳で調整しました。[信号にどう作用するかの計測](/ja/research/what-a-wall-does-to-sound)もしています。

## マップを渡す {#give-it-your-map}

リスナーの位置と 2 点の間にあるものを Earshot に伝え、ゲームループから `ear.update()` を呼びます。あとはオーディオグラフの中で処理されます。

```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();
```

どの `AudioNode` も音源にできます。効果音も、部屋の隅のジュークボックスの音楽も、プレイヤーの声も、同じルールに従います。

## 声をコントローラーに {#your-voice-as-a-controller}

`createLoudness` はマイクの音量を、部屋の環境音を基準にした 0〜3 のレベルに変換します。音声はアナライザーまでしか届かず、録音、送信、再生はされません。レベルと壁を組み合わせれば、ステルスのルールが 3 行で書けます。

```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();
```

どのレベルも `press()` でキーから入力できるので、マイクのないプレイヤーも同じゲームを遊べます。

## ゲーム世界の中のボイスチャット {#voice-chat-in-your-world}

0.2 の新機能 `ear.voice()` は、リモートプレイヤーのストリームを同じ壁と部屋に通します。`RTCPeerConnection` がすでにあるなら、これだけで足ります。ない場合は `joinVoiceRoom()` がプレイヤーどうしを接続します。

```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),
});
```

2 つの判断は、意図してゲーム側に残しています。1 つ目はマイクです。`canUseVoice` 関数が許可するまでマイクは開きません。同意の確認や、ポータルの `disableChat` のようなフラグはここで扱います。2 つ目は `ipPolicy` で、デフォルト値はありません。`"relay"` は他のプレイヤーから IP アドレスを隠し、音声は TURN のトラフィックとして課金されます。`"direct"` は安く済む代わりに、互いの IP アドレスが見えます。プレイヤー間のライブ音声には法的な義務を伴うことがあります。子どもも遊ぶゲームでは特に注意してください。公開する前に[ボイスチャットのドキュメント](/ja/earshot/#voice-chat)を読んでください。

## サーバーの費用 {#what-the-server-costs}

私たちは何もホストしません。[Worker テンプレート](https://github.com/debuffjp/earshot/tree/main/templates/worker)は自分のアカウントで動きます。プレイヤー間の接続メッセージを中継し、人数の上限を適用し、TURN の認証情報を発行します。ログインや BAN の確認を書ける `canJoin` 関数もあります。これらの確認を加えるまでは、ルームコードを知っている人なら誰でも参加できます。

費用がかさむのはリレーです。そこで、最悪のケースを計算しました。全員が話し続け、32 kbps の Opus にパケットのオーバーヘッドを加えると、1 ストリームあたり 1 時間で約 24 MB です。Cloudflare は TURN の送信トラフィックに対し、毎月 1,000 GB の無料枠を超えた分を 1 GB あたり $0.05 で課金します（[料金](https://developers.cloudflare.com/realtime/sfu/platform/pricing/)）。全員が全員に接続するので、n 人のルームには n x (n - 1) 本のストリームが流れます。4 人なら 12 本、8 人なら 56 本です。

| ルーム（全員リレー） | 1 時間あたりの GB | 無料枠を超えた後の 1 時間あたりの費用 | 無料枠に収まるルーム時間 |
| --- | --- | --- | --- |
| 4 人 | 0.29 | $0.014 | 約 3,470 |
| 8 人 | 1.34 | $0.067 | 約 740 |

実際のルームの費用はこれより少なくなります。十分に離れたプレイヤーへの送信は止まるからです。プッシュトゥトークではキーを離している間は無音を送るので、データは小さくなりますが、トラフィックはなくなりません。この表は TURN だけの計算です。ルームサーバーの Workers と Durable Objects の利用料は別に課金されます。

まず [Earshot のドキュメント](/ja/earshot/)を読み、プレイヤーとラジオの間に壁を 1 枚置いてみてください。