# feedback reference

Validate game-to-phone sound and vibration requests. Import from `@controller-plus/sdk` or `/sdk/src/feedback.mjs`. There is no package `feedback` subpath. Host sending methods are in [relay-host](sdk-relay-host.md#room-methods), [client](sdk-client.md#feedback-methods) and [node-host](sdk-node-host.md#feedback-and-phone-output).

## Feedback requests

| Request | Fields and defaults |
| --- | --- |
| Cue | `{ mode: 'cue', cue, volume = 1, id?, pattern? }`. `cue` and optional request `id`: 1–64 characters matching `[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}`. Volume 0…1. |
| Rumble | `{ mode: 'rumble', low, high, durationMs, id? }`. Strengths 0…1; duration 0…5000 ms. Required values have no validator defaults. |
| Stop | `{ mode: 'stop', channel = 'all', id? }`. Channel is `all`, `sound` or `vibration`. |

`validateFeedback(message): FeedbackRequest` returns only recognized fields. It validates a cue's ID syntax, not whether the cue exists. It throws `Error` for invalid request/id/channel/strength/duration/volume or a bad pattern. It accepts no arbitrary remote audio URL in a cue request.

## Patterns

`validatePattern(pattern): HapticEvent[]` returns a new array sorted by `atMs`. Each event is `{ atMs, durationMs, intensity, sharpness = 0.5 }`. Values must be finite: start 0…2000 ms, duration 1…2000 ms, end at or before 2000 ms, intensity/sharpness 0…1. At most 16 events; an empty pattern is valid. Invalid entries throw `Error`.

## Catalogs and audio

`validateCatalog(entries, endpoint): FeedbackCue[]` resolves URLs against the endpoint's origin (converting ws/wss to http/https). Returns a new normalized array. Every entry is `{ id, fallback, pattern?, sound? }`; IDs must be unique, use cue syntax and must not replace built-ins. `fallback` must name a built-in cue.

`sound` is `{ url, mime: 'audio/wav', sha256, maxBytes }`. `url` may be relative but must resolve to the connected game origin, be at most 2048 characters and contain no URL credentials or fragment. SHA-256 is exactly 64 lowercase hex characters. `maxBytes` is an integer 44…2,097,152. At most 32 entries, 33,554,432 declared audio bytes and 14,000 UTF-8 bytes of normalized catalog JSON. Validation throws `Error` (or a URL parsing error). It does not fetch, hash or decode audio.

`inspectFeedbackWav(buffer: ArrayBuffer | Uint8Array)` returns `{ channels, sampleRate, block, offset, bytes, duration }`, with duration in seconds. Requires PCM16 RIFF/WAVE, one fmt chunk before one data chunk, 1–2 channels, 8000–48000 Hz, at most five seconds and 2 MiB. `block` is bytes per sample frame; `offset` and `bytes` locate audio data. Truncation, duplicate/unordered chunks and invalid format throw `Error`.

`FEEDBACK_LIMITS` is frozen: `{ entries: 32, assetBytes: 2097152, cacheBytes: 33554432, patternEvents: 16, patternMs: 2000, rumbleMs: 5000 }`.

## Built-in cues

`BUILTIN_FEEDBACK` maps each ID below to `{ pattern, sound: { file, sha256, maxBytes, mime }, fallback }`. Files are shipped with the SDK under `assets/feedback/`. These descriptors name files; custom catalogs instead use `sound.url`.

| IDs | Use |
| --- | --- |
| `ui.tap`, `ui.confirm`, `ui.cancel` | Touch, acceptance, cancellation. |
| `game.jump`, `game.boost`, `game.hit`, `game.explosion` | Movement and impacts. |
| `game.pickup`, `game.countdown`, `game.ready` | Progress and round starts. |
| `game.warning`, `game.heartbeat`, `game.charge` | Warning and buildup. |
| `game.dial`, `game.unlock`, `game.win`, `game.lose` | Puzzles and results. |

These validation exports have no events. A controller may send `feedback-result` as a player event with `{ id, status }`; statuses are `played`, `fallback`, `unavailable`, `rate_limited`. A result is not proof that the player heard or felt it. Do not use feedback as the only way to communicate game state.

## Validate a cue and catalog

<!-- example: feedback-validation -->
```js
import { validatePattern, validateFeedback, validateCatalog,
  BUILTIN_FEEDBACK, FEEDBACK_LIMITS, inspectFeedbackWav } from '@controller-plus/sdk';

const pattern = validatePattern([{ atMs: 0, durationMs: 80, intensity: 0.5 }]);
const entries = validateCatalog([
  { id: 'round.start', fallback: 'game.ready', pattern },
], 'https://game.example');
console.log(entries, validateFeedback({ mode: 'cue', cue: 'round.start' }));
console.log(validateFeedback({ mode: 'rumble', low: 0.6, high: 0.2, durationMs: 100 }));
console.log(validateFeedback({ mode: 'stop' }), Object.keys(BUILTIN_FEEDBACK), FEEDBACK_LIMITS);
// A valid empty PCM16 WAV, used here only to illustrate inspection.
const wav = new Uint8Array(44), v = new DataView(wav.buffer);
for (const [at, text] of [[0, 'RIFF'], [8, 'WAVE'], [12, 'fmt '], [36, 'data']]) {
  wav.set(new TextEncoder().encode(text), at);
}
v.setUint32(4, 36, true); v.setUint32(16, 16, true);
v.setUint16(20, 1, true); v.setUint16(22, 1, true);
v.setUint32(24, 8000, true); v.setUint32(28, 16000, true);
v.setUint16(32, 2, true); v.setUint16(34, 16, true);
console.log(inspectFeedbackWav(wav).duration); // 0
```

See [Sounds, haptics and rumble](feedback.md) for a complete custom sound server, hashing, prefetch/fallback and player settings.
