# debuff で Earshot を使う

`@debuffjp/plugin-earshot` は Earshot をゲームにつなぎます。壁はタイルマップから読み取り、音は sfx バスに従い、シーンが変わると片付きます。

## インストール {#install}

```bash
npm install @debuffjp/plugin-earshot
```

`audio.buffer()` のある `@debuffjp/audio` が必要です。これは 0.29.2 の次のリリースで入ります。それより古いバージョンでは[手動で接続](#manual-hookup)してください。

## セットアップ {#set-up}

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

const ear = earshot({ audio, map, pos: () => player });
game.use(ear);
```

`pos` は、Earshot の他の部分と同じくワールド座標です。

| オプション | 意味 | デフォルト |
| --- | --- | --- |
| `audio` | ゲームのオーディオ。必須 | |
| `pos()` | リスナーの位置。必須 | |
| `map` | 当たり判定のグリッドが壁になるタイルマップ | なし |
| `material(x, y)` | 独自の名前を使うときの、タイルの素材 | 固いタイルは `"solid"` |
| `wallsBetween(a, b)` | 独自の壁の判定。`map` がないときだけ使います。座標はタイル単位 | なし |
| `reverbAt(p)` | ある地点のリバーブ。座標はタイル単位 | なし |
| `tile` | 1 タイルあたりのワールド単位 | マップのタイル幅、または `1` |
| `bus` | `"sfx"` か `"music"`。音はそのバスの音量とミュートに従います | `"sfx"` |
| `params`、`materials`、`reverbs` | 初期値。[`createListener`](./sound#listeners) と同じです | Earshot のデフォルト |

マップはライブに読み取るので、ドアを開けると音が通ります。一方通行の足場は壁になりません。タイルは `tileWidth` を使った正方形として扱います。マップの外は何もない空間として扱います。

```ts
const ear = earshot({
  audio,
  map,
  pos: () => player,
  material: (x, y) => (map.collision[y * map.width + x] === 1 ? (isGlass(x, y) ? "thin" : "solid") : null),
  reverbAt: (p) => (p.y > 20 ? "cave" : null),
});
```

`material` はデフォルトの判定を置き換えるので、上の例のように固いタイルかどうかを自分で確かめてください。

## play {#play}

音をデコードしてから、位置を指定して鳴らします。`at` は座標か、動くものなら関数です。

```ts
await audio.decode("door", assets.audio("door"));

ear.play("door", { at: door });
const hum = ear.play("radio", { at: () => radio, loop: true, volume: 0.8, rate: 1 });

hum.source.state; // "audible" | "out-of-range" | "muted"
hum.stop();
```

`play` は Earshot の[音源](./sound#sources)と `stop()` を返します。デコードしていない音はエラーを投げ、その旨を伝えます。音は鳴り始めるときにミックスを受け取るので、一瞬でもフィルターなしで鳴ることはありません。

## source {#source}

自分の `AudioNode` をマップに通します。

```ts
const source = ear.source(node, { at: () => jukebox, name: "jukebox" });
```

`name` は[インスペクト](#inspect)に表示する名前で、デフォルトは `"source"` です。シーンが変わると切り離されますが、ノードの開始と停止は自分で行います。

## 素材と壁 {#material}

`materials` は名前で壁の種類を追加または置き換えます。`material(x, y)` は各タイルの種類を返します。`map` がなければ、代わりに `wallsBetween` を渡します。

```ts
const ear = earshot({
  audio,
  pos: () => player,
  materials: { glass: { hz: 5000, db: -3 } },
  wallsBetween: (a, b) => wallsOnGrid(tiles, a, b),
});
```

デフォルト値と、壁が 2 枚以上のときのルールは[サウンド](./sound#materials)にあります。

## パラメータ {#params}

`fullRadius`、`silentRadius`、`panWidth` は `earshot` というパラメータグループに現れるので、あなたやエージェントがゲームの実行中に調整できます。プラグインはゲームの更新 3 回ごとにこれらをリスナーへコピーしてから音を更新します。この 3 つは `ear.listener.params` ではなくパラメータで設定してください。

```ts
game.params.group("earshot")?.set("silentRadius", 20);
```

それ以外はリスナー上でライブに変更できます。

```ts
ear.listener.params.smoothing = 0.2;
ear.listener.materials.thin = { hz: 3000, db: -4 };
```

## インスペクト {#inspect}

`__DEBUFF_DEV__.state().plugins.earshot` は各音の状態、距離、ゲイン、カットオフ、壁、リバーブを一覧にします。シナリオでプレイヤーに何が聞こえるかを確認できます。

```json
{
  "sounds": [
    { "name": "door", "state": "audible", "distance": 7, "gain": 0.13, "cutoff": 1200, "walls": ["solid"], "reverb": null }
  ]
}
```

## シーンの片付け {#scene-cleanup}

音は、終わったとき、止めたとき、シーンが変わったときに取り除かれます。新しいシーンの `enter()` で始めた音はそのシーンのものなので残ります。`game.dispose()` はリスナーも含めてすべて取り除きます。

## ボイスとリスナー {#voices-and-the-listener}

`ear.listener` は Earshot のリスナーで、ボイスなどプラグインが包んでいないものに使います。[ボイスルーム](./voice#voice-rooms)には `listener: ear.listener` として渡します。この方法で追加したボイスはインスペクトに現れず、シーンが変わっても終わらないので、自分で取り除いてください。更新はプラグインが他の音と一緒に行います。

## 手動で接続する {#manual-hookup}

debuff で Earshot を直接使うには、debuff のオーディオコンテキストとバスを渡します。Earshot はゲームの音量とミュートに従います。更新はフックから行います。

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

const ear = createListener(audio.ctx, {
  output: audio.bus("sfx"),
  tile: 16,
  pos: () => player,
  world: { wallsBetween: (a, b) => wallsOnGrid(tiles, a, b) },
});

const off = game.hooks.afterUpdate(() => ear.update());

const buffer = await audio.ctx.decodeAudioData(assets.audio("door"));
const node = new AudioBufferSourceNode(audio.ctx, { buffer });
const door = ear.source(node, { pos: () => doorPosition });
node.onended = () => door.remove();
ear.update();
node.start();
```

この場合、シーンの片付けは自分で行います。シーンを離れるときに `door.remove()` を、使い終わったら `off()` と `ear.dispose()` を呼んでください。