Command vs Clip
The same play(id) call automatically splits into two playback modes depending on the
kit manifest bucket. The calling code is one line either way and is unaware of the mode.
The split is determined by information in the EventMap (haptic file = kit manifest). See also Fire vs. Clip for a shared conceptual explanation.
Two modes
Section titled “Two modes”| manifest bucket | mode | what happens | deploy required |
|---|---|---|---|
events | command | the SDK sends PLAY, and the device plays its built-in clip | yes (write the kit in Studio) |
stream_events | clip | the SDK loads the WAV (clipBase + clipLoader) and UDP-streams it | no |
import { connect, EventMap } from "@hapbeat/sdk";
const manifest = await fetch("/my-kit/my-kit-manifest.json").then((r) => r.json());const hb = await connect({ eventMap: EventMap.fromManifest(manifest), clipBase: "/my-kit/stream-clips/", // where clip-mode WAVs live});
hb.play("sample-kit.sine_100hz"); // command → the device plays its built-in cliphb.play("rain.loop"); // clip → the SDK streams a WAVhb.stop("rain.loop"); // clip ends the stream currently playingIf you do not pass eventMap, the SDK has no bucket information, so everything is sent as
command (in which case the default gain is 1.0).
Which to choose
Section titled “Which to choose”| command | clip | |
|---|---|---|
| recommended | mass-produced / production short one-shots | prototyping / long-form / frequently swapped stages |
| latency | small, stable | a bit larger, environment-dependent |
| pre-deploy | required (write the kit in Studio) | not required (just drop the WAV) |
| swapping the haptic | re-deploy the kit | just replace the WAV |
When in doubt, the basic rule is clip while prototyping, command once it’s settled. command is the lightest and most stable since it just calls the device’s built-in clip, while clip lets you stream a WAV on the spot so you can iterate without the hassle of deployment.
clip resolution (clipBase + clipLoader)
Section titled “clip resolution (clipBase + clipLoader)”In clip mode, the SDK concatenates the manifest’s clip filename to clipBase and loads
the WAV with clipLoader before streaming. The default loader switches per environment.
| environment | clipBase | default clipLoader |
|---|---|---|
| Node | directory path | fs.readFile |
| Browser | URL prefix | fetch |
// Browser: resolve a kit served as static assets by URLconst hb = await connect({ eventMap: EventMap.fromManifest(manifest), clipBase: "/my-kit/stream-clips/",});
// Node: resolve by directory pathconst hb = await connect({ eventMap: EventMap.fromManifest(manifest), clipBase: "./kits/my-kit/stream-clips/",});If you want to load from a bundle or IndexedDB, override clipLoader.
const hb = await connect({ eventMap: EventMap.fromManifest(manifest), clipLoader: async (ref) => loadFromBundle(ref), // return ArrayBuffer | Uint8Array});If you play a clip event without clipBase / clipLoader set, the SDK cannot read the
WAV, emits a warning, and plays nothing. command-mode events do not need this.
gain is not applied twice
Section titled “gain is not applied twice”The default gain is the kit manifest’s intensity. Specifying play’s gain explicitly
overrides it; it is not applied twice. Even in clip mode, the SDK does not process the PCM;
it folds gain only into STREAM_BEGIN.gain and sends it, and the device applies it once
(the same meaning as command’s gain).
hb.play("rain.loop"); // fires with intensity (the kit default)hb.play("rain.loop", { gain: 0.3 }); // overridden by the callergain is an absolute value 0..1, clamped by the SDK. There is no API to modulate gain over
the course of a clip already playing (for continuous modulation, see
Not implemented, which uses openStream).
clip WAV constraints
Section titled “clip WAV constraints”- Prepare clip WAVs as 16 kHz mono PCM16. The device plays at 16 kHz and the SDK does not resample (non-16 kHz only produces a warning).
- Only one stream can run at a time (per session). Starting a new clip,
streamPcm, oropenStreamautomatically ends the previous stream. - If you write the same id into both
eventsandstream_events(Studio’s BOTH), then on manifest loadstream_eventsoverrides last, so the clip side wins.
clip targeting difference between Browser and Node
Section titled “clip targeting difference between Browser and Node”A command-mode target works the same on both transports. In clip mode, however, behavior
differs when going through the helper (Browser).
- Browser (via helper WS): clip playback reaches every device the helper recognizes
(per-device clip targeting is unsupported). Also,
targetTimeUsis ignored through the helper, so playback is always immediate. - Node (direct UDP): clip can be addressed with
targetjust like command.
For details on the target syntax, see Address System.