Controller+BETADownload

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.