# ボイス

0.2 で追加しました。Earshot は、声がゲームの世界の中にあるように聞こえるようにします。既存の `RTCPeerConnection` か、後述の[ボイスルーム](#voice-rooms)を使えます。私たちがサーバーをホストすることはありません。

## ボイスチャット {#voice-chat}

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

const mic = createVoiceMic({ canUseVoice: () => settings.voiceAllowed });
const { ok, error } = await mic.start(); // タップやキー入力から呼びます。
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、voice.levelDb は UI に使えます。
};
```

声は他の音源と同じように壁、部屋、パンを通り、同じように `ear.update()` が必要です。`ear.voice()` には `OfflineAudioContext` ではなく `AudioContext` が必要です。

| ボイスのプロパティ | 意味 |
| --- | --- |
| `voice.muted` | このプレイヤーを手元でミュートします |
| `voice.levelDb` | 距離と壁を通す前の音量（dB） |
| `voice.speaking` | 発話インジケーター用。距離と壁を無視するので、範囲が重要なら `state` も確認してください |
| `voice.state`、`voice.mix`、`voice.reverb` | 他の[音源](./sound#sources)と同じです |
| `voice.remove()` | このボイスを切り離します |

`speaking` は `speakingDb` を超えるとオンになります。`speakingDb` は `ear.voice()` のオプションで、デフォルトは -50 です。

## マイクの操作 {#mic-controls}

マイクはゲームが求めるまでオフで、何も録音しません。

```ts
mic.muted = true;
mic.pushToTalk = true;
mic.talk(true); // pushToTalk がオンの間、押している間だけ話せます。
mic.talk(false);
mic.live; // 音声を送信中なら true。プレイヤーに表示してください。
mic.stop();
```

`canUseVoice` は必須で、false の間はマイクを開きません。同意の確認や、ポータルの `disableChat` などのフラグに使ってください。このとき `start()` は `{ ok: false, error: "not-allowed" }` を返します。確認するのは `start()` を呼んだときだけなので、後から許可がなくなったら自分で `mic.stop()` を呼んでください。

`start()` は reject しません。ブラウザに拒否されると、`error` はブラウザのエラー名です。

| オプション | 意味 | デフォルト |
| --- | --- | --- |
| `canUseVoice()` | このプレイヤーがボイスを使えるかの確認。必須 | |
| `echoCancellation` | `true`、`false`、`"all"`、`"remote-only"`。`"all"` は他の音声のキャンセルも、可能な範囲でブラウザに求めます | `true` |
| `pushToTalk` | オンの間、マイクは `talk(true)` のときだけ送信します | `false` |

`mic.applied` は、ブラウザが実際に適用した設定です。ノイズ抑制と自動ゲインは常に要求します。

## ボイスルーム {#voice-rooms}

接続の仕組みがまだなければ、`joinVoiceRoom()` を使えます。[自分のアカウントにデプロイする](./self-hosting)小さな Cloudflare Worker でプレイヤーをつなぎます。私たちはホストしません。招待制のルーム、人数の上限、短命な TURN 認証情報を備えていて、ログインや BAN の確認は `canJoin` 関数に書きます。

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

const code = createRoomCode(); // フレンドに共有します。
const joined = await joinVoiceRoom({
  url: "wss://earshot-voice.you.workers.dev",
  code,
  mic,
  listener: ear,
  ipPolicy: "relay", // または "direct"（後述）。
  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` を開始しておいてください。`pos` は最初の位置が届く前のプレイヤーにも呼ばれるので、代わりの値を返せるようにします。コードを知っていれば誰でも参加できるので、共有はフレンドだけにしてください。

`joinVoiceRoom()` は reject しません。`ok: true` はルームに入ったことを表し、各プレイヤーとの接続はまだ確立中かもしれません。

| オプション | 意味 |
| --- | --- |
| `onPeer(peer, voice)` | プレイヤーの声が届きました |
| `onLeave(peer)` | プレイヤーが退出しました。自分が退出するときも全員分呼ばれます |
| `onMessage(peer, message)` | `room.send` の小さなメッセージ。遅れて届くことも、届かないこともあります |

| ルームのプロパティ | 意味 |
| --- | --- |
| `room.id` | このプレイヤーの ID |
| `room.peers` | ID ごとの各プレイヤーの `Voice` |
| `room.connection(peer)` | UI 用の接続状態：`"connecting"`、`"connected"`、`"failed"` など |
| `room.send(message)` | 接続中の全プレイヤーに JSON を送ります。Worker を通さず直接送ります |
| `room.leave()` | ルームから退出します。マイクは `mic.stop()` まで開いたままです |

## ipPolicy {#ippolicy}

`ipPolicy` はプライバシーに関わる判断なので必須です。`"relay"` は他のプレイヤーから IP アドレスを隠し、中継できないときは接続に失敗します。すべての音声は TURN のトラフィックとして課金されます。`"direct"` は安く済みますが、互いの IP アドレスが見えます。

費用の表は[セルフホスト](./self-hosting#costs)にあります。

## 接続とエラー {#connections-and-errors}

全員が全員に接続するので、人数に応じてアップロードが増えます。十分に離れたプレイヤー（`silentRadius` の 1.25 倍より遠く）への送信は止まります。

エラーは `joined.error` に返ります。

| エラー | 意味 |
| --- | --- |
| `room-full` | ルームが Worker の `MAX_PLAYERS` に達しています |
| `no-turn-server` | `ipPolicy` が `"relay"` なのに、Worker が TURN サーバーを提供しませんでした |
| `mic-not-started` | `mic.start()` が成功していません |
| `connection-failed` | Worker に接続できないか、接続を拒否されました |

切断された接続は自動では再接続しないので、`room.connection(peer)` で状態を表示してください。

## 開発者の責任 {#your-duties}

**これらの機能だけでは、法令への準拠にはなりません。** プレイヤー間のライブ音声には、米国の COPPA や英国の Online Safety Act などの義務を伴う場合があります。子ども向けのゲームでは特に注意してください。年齢確認、BAN の強制、モデレーション、通報の受付はブラウザのライブラリでは扱えません。ゲーム側で計画してください。ボイスはデフォルトでオフにし、フレンドに限定してください。これは法的助言ではありません。

## テスト環境 {#tested-on}

テストしたのは、同じネットワーク上の PC と iPhone、およびデスクトップの Chromium と Firefox の間のリレー接続です。より大きなグループは未テストです。

[Earshot 0.2 のリリース記事](/ja/blog/earshot-sound-through-walls)でも、これらの機能を紹介しています。