# Earshot with debuff

`@debuffjp/plugin-earshot` connects Earshot to your game: walls come from the tilemap, sounds follow the sfx bus, and scenes clean up after themselves.

## Install {#install}

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

It needs `@debuffjp/audio` with `audio.buffer()`, which arrives in the release after 0.29.2. On older versions, use the [manual hookup](#manual-hookup).

## Set up {#set-up}

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

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

`pos` is in world units, as it is everywhere in Earshot.

| Option | Meaning | Default |
| --- | --- | --- |
| `audio` | Your game's audio. Required | |
| `pos()` | Listener position. Required | |
| `map` | A tilemap whose collision grid gives the walls | None |
| `material(x, y)` | A tile's material, when you want your own names | `"solid"` for solid tiles |
| `wallsBetween(a, b)` | Your own wall check, used only when there's no `map`. Points are in tiles | None |
| `reverbAt(p)` | The reverb at a point, in tiles | None |
| `tile` | World units per tile | The map's tile width, or `1` |
| `bus` | `"sfx"` or `"music"`, so sounds follow that bus's volume and mute | `"sfx"` |
| `params`, `materials`, `reverbs` | Starting values, as on [`createListener`](./sound#listeners) | Earshot's defaults |

The map is read live, so an opened door lets sound through. One-way platforms aren't walls. Tiles are treated as square, using `tileWidth`. Tiles outside the map are open air.

```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` replaces the default check, so test for solid tiles yourself as above.

## play {#play}

Decode a sound first, then play it at a position. `at` is a point, or a function for something that moves.

```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` returns the Earshot [source](./sound#sources) and a `stop()`. A sound that isn't decoded throws an error saying so. The sound gets its mix as it starts, so it never plays a moment unfiltered.

## source {#source}

Send your own `AudioNode` through the map.

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

`name` labels it in [inspect](#inspect) and defaults to `"source"`. On a scene change it's disconnected, and starting or stopping the node stays up to you.

## material and walls {#material}

`materials` adds or replaces wall types by name, and `material(x, y)` says which one each tile is. Without a `map`, pass `wallsBetween` instead:

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

The default values and the two-or-more-walls rule are in [Sound](./sound#materials).

## params {#params}

`fullRadius`, `silentRadius` and `panWidth` appear in a param group called `earshot`, so you and your agents can tune them while the game runs. The plugin copies them to the listener on every third game update, then updates the sounds, so set these three through params and not on `ear.listener.params`.

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

Everything else stays live on the listener:

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

## inspect {#inspect}

`__DEBUFF_DEV__.state().plugins.earshot` lists each sound's state, distance, gain, cutoff, walls and reverb, so scenarios can check what a player hears.

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

## Scene cleanup {#scene-cleanup}

A sound is removed when it ends, when you stop it, or when the scene changes. A sound started in the new scene's `enter()` belongs to that scene and stays. `game.dispose()` removes everything, including the listener.

## Voices and the listener {#voices-and-the-listener}

`ear.listener` is the Earshot listener, for voices and anything else the plugin doesn't wrap. Pass it to [voice rooms](./voice#voice-rooms) as `listener: ear.listener`. Voices added this way aren't in inspect and don't end with the scene, so remove them yourself. The plugin updates them along with everything else.

## Manual hookup {#manual-hookup}

To use Earshot with debuff directly, pass debuff's audio context and a bus, so Earshot follows the game's volume and mute. Update it from a hook.

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

Here scene cleanup is yours: call `door.remove()` when you leave, and `off()` and `ear.dispose()` when you're done.