# 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 {#mic-input}

```ts
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](./#limits).

## Calibration {#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:

```ts
await loud.recalibrate(); // Ask the player to stay quiet first.
loud.overFloorDb; // dB above the noise floor, or null with no calibrated mic
```

## Levels {#levels}

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:

```ts
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 reading
```

`update` returns 0 until `calibrate` has run.

## heardDb {#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.

```ts
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:

```ts
heardDb(loud.level, mix, [-Infinity, 6, 12, 18]);
```

## Accessibility rules {#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:

```ts
addEventListener("keydown", (e) => { if (e.code === "KeyF") loud.press(2); });
addEventListener("keyup", (e) => { if (e.code === "KeyF") loud.press(0); });
```