Skip to content
EN

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.

what it holdswho makes it
kit manifest (<kit>-manifest.json)the kit’s content: intensity / clip / command or clipauto-generated by Hapbeat Studio
app codewhen 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-site target takes precedence)

If you omit the target, it becomes "" (broadcast to all).

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 on clipBase.

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 mode
hb.play("rain.loop"); // clip mode (streams the WAV under kits via fs.readFile)

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 bundler
import 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.

  1. Edit the kit in Hapbeat Studio (clips, intensity, command/clip).
  2. Place the kit folder (manifest + stream-clips/) in your app (web: public/, node: any dir).
  3. Read the manifest with fetch (web) / readFile (node) and pass EventMap.fromManifest(manifest) to connect.
  4. If you use clip mode, point clipBase at the WAVs’ location (URL prefix / dir).
  5. The code just calls hb.play("event.id"). The target comes from the call site or defaultTarget.