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
npm install @debuffjp/plugin-earshotIt needs @debuffjp/audio with audio.buffer(), which arrives in the release after 0.29.2. On older versions, use the manual hookup.
Set up
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 | 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.
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
Decode a sound first, then play it at a position. at is a point, or a function for something that moves.
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 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
Send your own AudioNode through the map.
const source = ear.source(node, { at: () => jukebox, name: "jukebox" });name labels it in 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
materials adds or replaces wall types by name, and material(x, y) says which one each tile is. Without a map, pass wallsBetween instead:
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.
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.
game.params.group("earshot")?.set("silentRadius", 20);Everything else stays live on the listener:
ear.listener.params.smoothing = 0.2;
ear.listener.materials.thin = { hz: 3000, db: -4 };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.
{
"sounds": [
{ "name": "door", "state": "audible", "distance": 7, "gain": 0.13, "cutoff": 1200, "walls": ["solid"], "reverb": null }
]
}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
ear.listener is the Earshot listener, for voices and anything else the plugin doesn't wrap. Pass it to 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
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.
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.