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, client and node-host.
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
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 for a complete custom sound server, hashing, prefetch/fallback and player settings.