Loudness
createLoudness turns the mic into a level from 0 to 3 that your game can read. Audio goes to an analyser and no further: Earshot doesn't record it, send it or play it back.
Mic input
import { createLoudness } from "@debuffjp/earshot";
const loud = createLoudness(ctx);
const off = loud.onLevel((level) => { if (level >= 2) monster.hear(player); });
// From a button: the player stays quiet for 3 seconds while it measures the room.
const { ok, error } = await loud.enableMic();
loud.level; // 0 to 3
loud.micStatus; // "off" | "calibrating" | "on"
loud.disableMic();
off();createLoudness needs an AudioContext, so resume it from the same tap or key press. enableMic() never rejects. A denied mic returns { ok: false, error }, where error is the browser's error name, and press keeps working.
Browsers can ignore what you ask for, so read loud.applied for the settings you got. Echo cancellation, noise suppression and auto gain are off by default because they flatten loudness. Pass browserProcessing: true to ask for them.
The mic needs HTTPS or localhost, and an iframe needs allow="microphone". Opening it can change game audio on iPhones: see Limits.
Calibration
enableMic() opens the mic, listens for calibrateMs (3000 by default) and resolves when it's done. The 20th percentile of those readings becomes the noise floor. Tell the player to stay quiet first, and show micStatus while it's "calibrating".
If the room changes, measure it again:
await loud.recalibrate(); // Ask the player to stay quiet first.
loud.overFloorDb; // dB above the noise floor, or null with no calibrated micLevels
Levels are relative to the noise floor, so loud and quiet rooms behave alike.
| Level | Above the noise floor |
|---|---|
| 0 | Under 10 dB |
| 1 | 10 dB or more |
| 2 | 20 dB or more |
| 3 | 30 dB or more |
| Option | Meaning | Default |
|---|---|---|
steps | dB above the floor for levels 1, 2 and 3 | [10, 20, 30] |
attackMs | How long a louder sound must last before the level rises | 150 |
releaseMs | How long it must stay quiet before the level falls | 400 |
hysteresisDb | A level holds until the sound drops this far under its step | 5 |
loud.level is the louder of the mic and press. onLevel fires only when that changes, and returns a function that removes the callback.
To feed in your own readings, createLevelDetector is the same logic with no mic:
import { createLevelDetector } from "@debuffjp/earshot";
const detector = createLevelDetector({ steps: [6, 12, 18] });
detector.calibrate([-62, -60, -61, -60]); // dB readings taken while quiet
detector.update(-40, 16); // dB and milliseconds since the last readingupdate returns 0 until calibrate has run.
heardDb
To make walls and distance matter in your game rules, work out how loud the noise is where the monster stands. The threshold is yours to tune; 6 dB felt right in our demo.
import { computeMix, heardDb, wallsOnGrid, DEFAULT_MATERIALS, DEFAULT_PARAMS } from "@debuffjp/earshot";
const walls = wallsOnGrid(tiles, player, monster); // Both in tile units.
const mix = computeMix(player, monster, walls, DEFAULT_PARAMS, DEFAULT_MATERIALS);
if (loud.level >= 2 && heardDb(loud.level, mix) >= 6) monster.wake();heardDb(level, mix) adds the mix's gain, in dB, to the level's loudness. Level 0 and an out-of-range mix both give -Infinity. If you changed steps, pass matching values as the third argument, with level 0 first:
heardDb(loud.level, mix, [-Infinity, 6, 12, 18]);Accessibility rules
- Always give players a key or button through
press(). - Don't require words or long strain.
- Tell players what the mic is for before asking.
press holds a level until you change it, and everything the mic does works without one:
addEventListener("keydown", (e) => { if (e.code === "KeyF") loud.press(2); });
addEventListener("keyup", (e) => { if (e.code === "KeyF") loud.press(0); });