Project Structure
When you separate the kit (what Studio produces) from your app code, the code just plays an event id. This page shows how to lay that split out into files, for web and node respectively.
Two layers
Section titled “Two layers”| what it holds | who makes it | |
|---|---|---|
kit manifest (<kit>-manifest.json) | the kit’s content: intensity / clip / command or clip | auto-generated by Hapbeat Studio |
| app code | when and where to fire (play(id) / target) | written by the developer |
The manifest is “a description of the kit’s content” and does not hold which device/body part to play on (target). Firing timing and target are the app’s concern, so they are decided in code. See EventMap reference for details.
JS has no Python-style haptic file overlay
Section titled “JS has no Python-style haptic file overlay”The Python SDK has haptics.json (an overlay that references a kit to add target/gain), but
the JS SDK does not. The JS EventMap is manifest-only; it just carries each event’s
intensity / clip. There are two ways to decide the target:
- Specify at the call site:
hb.play("sample-kit.sine_100hz", { target: "player_1/chest" }) - Default value:
connect({ defaultTarget: "*/chest" })(the call-sitetargettakes precedence)
If you omit the target, it becomes "" (broadcast to all).
web layout (static serving + fetch)
Section titled “web layout (static serving + fetch)”Serve the manifest and clip WAVs as static assets and fetch them using clipBase as a
URL prefix. The browser’s default clipLoader is fetch.
my-web-app/├── src/│ └── main.ts ← caller (connect() + play(id))└── public/ └── my-kit/ ├── my-kit-manifest.json ← Studio-generated. fetch → EventMap.fromManifest └── stream-clips/ └── rain.wav ← the WAV the SDK streams in clip mode (16 kHz PCM16)import { connect, EventMap } from "@hapbeat/sdk";
const manifest = await fetch("/my-kit/my-kit-manifest.json").then((r) => r.json());
const hb = await connect({ appName: "MyWebXR", eventMap: EventMap.fromManifest(manifest), clipBase: "/my-kit/stream-clips/", // clip references = clipBase + the filename in the manifest});
hb.play("sample-kit.sine_100hz"); // command mode (plays the clip on the device)hb.play("rain.loop"); // clip mode (fetches the WAV under public and streams it)Clip references resolve as a simple concatenation of
clipBase + <the clip field in the manifest>. Mind the trailing slash onclipBase.
node layout (directory + fs.readFile)
Section titled “node layout (directory + fs.readFile)”clipBase is a directory path. node’s default clipLoader is fs.readFile, which reads
the WAV from disk.
my-node-app/├── index.mjs ← caller (connect() + play(id))└── kits/ └── my-kit/ ├── my-kit-manifest.json ← Studio-generated └── stream-clips/ └── rain.wav ← clip-mode WAV (16 kHz PCM16)import { readFile } from "node:fs/promises";import { connect, EventMap } from "@hapbeat/sdk";
const manifest = JSON.parse( await readFile("kits/my-kit/my-kit-manifest.json", "utf8"),);
const hb = await connect({ appName: "MyApp", eventMap: EventMap.fromManifest(manifest), clipBase: "kits/my-kit/stream-clips/", // directory path});
hb.play("sample-kit.sine_100hz"); // command modehb.play("rain.loop"); // clip mode (streams the WAV under kits via fs.readFile)Custom clipLoader
Section titled “Custom clipLoader”When clipBase + the default loader is not enough (pre-bundled assets, IndexedDB, CDN, etc.),
swap in clipLoader. It receives clipBase + def.clip as the argument ref and is a function
that returns ArrayBuffer | Uint8Array.
// Example reading a clip from IndexedDB (Browser)const hb = await connect({ eventMap: EventMap.fromManifest(manifest), clipBase: "", // can be empty if you don't need a key prefix clipLoader: async (ref) => { const buf = await idbGet(ref); // ref is clipBase + 'rain.wav', etc. return buf; // ArrayBuffer },});// Example using a WAV imported via a bundlerimport rainWavUrl from "./kits/my-kit/stream-clips/rain.wav?url";
const hb = await connect({ eventMap: EventMap.fromManifest(manifest), clipBase: "", clipLoader: async (ref) => { const url = ref === "rain.wav" ? rainWavUrl : ref; return fetch(url).then((r) => r.arrayBuffer()); },});clip-mode WAVs must be 16 kHz PCM16. The SDK does not resample (non-16 kHz produces a warning). For details on clip mode, see Command vs Clip.
Authoring flow
Section titled “Authoring flow”- Edit the kit in Hapbeat Studio (clips, intensity, command/clip).
- Place the kit folder (manifest +
stream-clips/) in your app (web:public/, node: any dir). - Read the manifest with
fetch(web) /readFile(node) and passEventMap.fromManifest(manifest)toconnect. - If you use clip mode, point
clipBaseat the WAVs’ location (URL prefix / dir). - The code just calls
hb.play("event.id"). The target comes from the call site ordefaultTarget.
Read next
Section titled “Read next”- EventMap reference — the EventMap-to-manifest correspondence
- Command vs Clip — choosing between command and clip
- Event ID and Kit — the relationship between event id and kit