Soundbook
首页
产品
体验
了解
注册登录
文档首页

游戏开发者

  • 游戏开发者:5 分钟接入 .pulse
  • Pulse Game 接入说明书
  • 故障排查 FAQ规划中
  • Unity 接入 .pulse规划中
  • Unreal 接入 .pulse规划中

音乐设计师

  • 制作Pulse Designer 手册:Builder 高级设置
  • 交付.pulse 格式与交付要求
  • 上架在 Soundbook 发布作品
PULSE · WEB / HTML5 完整接入说明书 · 对应 @clxgame/pulse-game v0.1.0

Pulse Game 接入说明书

更新于 2026-07-19

本页目录

  • 1. 你会拿到什么
  • 2. 安装
  • 3. 放置与加载 `.pulse`
  • 4. 从用户手势开始播放
  • 5. 游戏状态映射
  • 6. 参数映射
  • 7. 普通接入不需要额外预加载
  • 8. 生命周期与事件
  • 9. 可选:数据驱动事件映射
  • 10. 静态部署、CDN 与缓存
  • 11. 进阶:读取 manifest 或使用自定义 loader
  • 12. 完整 HTML5 联调 Host
  • 13. 上线前检查清单
  • 14. 当前边界

本说明书面向要在 Web / HTML5 游戏中接入一个已交付 .pulse 音乐包的游戏开发者。

第一次接入请先完成游戏开发者 5 分钟接入,再回到本文处理预加载、事件、部署和错误降级。不确定该读哪份文档时,请从角色导航开始。

它只描述当前已实现、可验证的能力:加载包含内嵌音频的 .pulse、播放、状态切换、参数控制、预加载、事件监听和释放。当前生产接入边界是 @clxgame/pulse-game;Unity、Godot、Unreal 尚没有可作为生产承诺的 runtime/插件。

1. 你会拿到什么

一个 .pulse 文件是一份可运行的互动音乐包,包含:

  • 音频 bytes(通常为内嵌 wav/mp3/ogg);
  • 状态、片段、layer、转场和参数定义;
  • 可选的授权、兼容性 metadata 与开发者接入合同。

游戏侧只应依赖包内 metadata.publicInterface 明确公开的 state ID 和 parameter ID。未列出的状态和参数,以及所有 resource、segment、layer ID,都属于音乐包内部编排,后续内容更新时可能变化。

在开始前,用 Pulse Desktop 打开最终 .pulse,从帮助页复制自动生成的 TypeScript 合同。例如:

export const pulseContract = {
  source: "declared",
  initialStateId: "exploration",
  states: {
    exploration: "exploration",
    combat: "combat",
  },
  parameters: {
    tension: {
      id: "tension",
      min: 0,
      max: 1,
      defaultValue: 0.2,
      unit: null,
    },
  },
} as const;

生成结果还包含 state/parameter 的字面量类型,可直接放进项目的 pulse-contract.ts。每次更新音乐包时,同时更新这份生成文件,不要手写第二套 ID 清单。

接入合同有三条兼容规则:

  • 新包明确声明合同时,模块第一个状态就是公开的 initialStateId,参数清单可以为空;SDK 只公开合同允许的 ID,内部 ID 即使存在也不能通过游戏 facade 调用;
  • 旧包缺少合同时,SDK 会推断全部状态和参数并显示警告,以保证旧内容仍能运行;请先让内容方确认,不要把警告当作正式合同;
  • 已声明但损坏或版本不受支持的合同不会退回“全部公开”,因此不会意外泄露内部 ID;加载或代码生成会给出明确错误。

旧包的推断结果只存在于接入工具中,不会在普通保存时被静默写回。必须由 Pulse 设计师在 Builder 的“游戏接入合同”中主动确认并保存。

2. 安装

pnpm add @clxgame/pulse-game

游戏代码从 @clxgame/pulse-game 导入 runtime。除非需要读取 manifest metadata、检查包内容或实现自定义资源管线,否则不需要直接依赖 @clxgame/pulse-engine。

3. 放置与加载 .pulse

把文件作为游戏静态资源发布。例如 Vite/Web 项目可以放在:

public/packs/forest-combat.pulse

它会以 /packs/forest-combat.pulse 提供。

推荐入口会替你完成下载、HTTP 状态检查、严格校验、runtime 创建和音频解码,但不会自动播放:

import {
  loadPulseGame,
  type PulseGameRuntime,
} from "@clxgame/pulse-game";

export async function loadSceneMusic(
  url = "/packs/forest-combat.pulse",
): Promise<PulseGameRuntime> {
  return loadPulseGame(url);
}

loadPulseGame() 默认执行严格校验;初始化失败时会自动释放未完成的 runtime。把错误上报到游戏日志/监控,并回退到静态 BGM 或静音即可。只有已有二进制资源管线或自定义 loader 的进阶 host 才需要后文的低阶 factory。

本地开发时从 Pulse Desktop 推送音乐包

如果项目使用 Vite,可以用 @clxgame/pulse-game/vite 的开发插件把 Desktop 写出的 .pulse 暴露给游戏。它只添加本地开发路由,不会进入生产 服务器:

import { createPulseGameLiveSyncPlugin } from "@clxgame/pulse-game/vite";

createPulseGameLiveSyncPlugin({
  livePackPath: "/absolute/path/to/live-sync.pulse",
  fallbackPackPath: "/absolute/path/to/forest-combat.pulse",
  contract: gameSyncContract,
});

gameSyncContract 的字段是完整必填的:协议版本、游戏 ID/名称、adapter、 runtime API 版本、必需 state、必需/可选 parameter 和 capabilities。SDK 会在 TypeScript 编译期检查;Desktop 连接时还会把网络响应当作不可信数据再次严格 校验,缺字段就不会写包。

游戏侧在开发模式轮询 /__pulse-sync/status,版本变化后从 /__pulse-sync/pack 创建一个候选 runtime;只有候选包完成合同校验和预加载 后,才释放旧 runtime。这样错误的编辑不会打断当前音乐。生产模式仍加载固定 的静态 .pulse URL。

同一个开发桥还提供 /__pulse-sync/telemetry。游戏同源页面可以上报只读运行 快照和一小段因果事件,Pulse Desktop 则显示:游戏心跳、当前公开 state、公开 parameter、正在发声的 layer、待执行转场,以及 game signal -> Pulse action -> runtime event。遥测是设计师调试信息,不是新的游戏控制 API;layer ID 也不是 可依赖的稳定接入合同。

在 Pulse Deck 中打开“让游戏驱动本地预览”后,Desktop 会用当前游戏的公开 state/parameter 驱动正在编辑的工程。所有输入都会再次经过 .pulse 的 metadata.publicInterface allowlist 与参数范围校验;信号断开、数据畸形或目标 不匹配时不会继续控制本地引擎。游戏暂停或旁路不会自动停止 Desktop,因此可以 静音游戏标签页里的 Pulse,只听本地工程做 A/B 对比。这里是两路独立输出:如需 只听游戏原始 SFX,还要同时关闭 Desktop 的“让游戏驱动本地预览”。

出于内容保护,/status 和 /pack 只允许游戏同源访问;不能被任意网页跨域 读取。遥测只接受同协议、同主机游戏页面的受限 JSON 上报,并且只有精确白名单 中的 Desktop origin 可以读取;请求体、字段数量、时间戳和事件历史都有上限。 /target 默认也只额外允许 Pulse Desktop 的本地开发/Tauri origin。若你的 Desktop 壳运行在其他可信 origin,可显式传 allowedTargetOrigins,不要使用 通配符 *。

仓库内可以直接运行 pnpm dev:underrun-live 查看完整范例。这个命令同时启动 Pulse Desktop、SDK watcher 和 UNDERRUN;在 Desktop 的 工具 → 游戏联调面板 中先连接游戏,再点 同步到游戏。具体操作见 examples/underrun-pulse。

4. 从用户手势开始播放

浏览器通常禁止在没有用户手势时恢复音频。下载和预加载可以早做,但 play() 必须放在 Start、Continue、点击画面等用户操作之后。

import { pulseContract } from "./pulse-contract";

const music = await loadSceneMusic();
startButton.disabled = false;

startButton.addEventListener("click", async () => {
  music.setGameState(pulseContract.initialStateId);
  await music.play();
});

在加载页或场景准备阶段先完成 loadSceneMusic(),成功后再启用开始按钮。play() 只负责从用户手势恢复并开始播放;若它失败,music.state.playing 会保持 false。

5. 游戏状态映射

把游戏事件集中映射到生成合同里的 pack state,不要把裸字符串分散在业务代码中。

import { pulseContract } from "./pulse-contract";

const musicStates = {
  exploration: pulseContract.states.exploration,
  combat: pulseContract.states.combat,
} as const;

function onEncounterStarted() {
  music?.setGameState(musicStates.combat, {
    transitionMode: "immediate",
  });
}

function onEncounterEnded() {
  music?.setGameState(musicStates.exploration, {
    transitionMode: "nextBar",
  });
}

支持的 mode 是:

mode行为
immediate立即切换,使用 pack 定义的交叉淡入淡出时长或默认值。
nextBar在下一个小节边界切换。
nextEnd在当前片段结束时切换。
sameTime保持当前时间位置切换。

不传 transitionMode 时,runtime 优先使用 pack 内对应 transition 的 mode;若 pack 没有这条 transition,则使用 immediate。同一时刻只有一个延迟转场有效,后一次请求会替换前一次 pending 请求。

传入不存在或未公开的 state ID 会同步抛出 RangeError,并保留当前可听状态。因此应在开发环境把它视为合同错误,而不是吞掉。旧包没有显式合同时,为兼容历史内容,推断出的所有状态暂时都视为公开。

trigger() 目前是 setGameState() 的别名,用于兼容接入层命名;当前 P0 没有独立的 stinger/trigger 语义。请不要把它当作通用音效触发器。

6. 参数映射

参数 ID 和数值范围由 pack 定义。未知 parameter ID、NaN 和无穷值会立即抛错;有限数值超出范围时,游戏 runtime 会自动限制到 pack 声明的最小值或最大值。游戏侧仍应按业务含义归一化并适度节流。

const tensionParameter = pulseContract.parameters.tension;

function updateMusicPressure(playerHealth: number, maxHealth: number, enemyCount: number) {
  const healthPressure = 1 - playerHealth / maxHealth;
  const enemyPressure = Math.min(enemyCount / 6, 1);
  const tension = Math.max(0, Math.min(1, Math.max(healthPressure, enemyPressure)));

  music?.setGameParameter(tensionParameter.id, tension);
}

对每帧变化的数值设置阈值或节流,避免无意义写入:

let previousTension = -1;

function tickMusic(tension: number) {
  if (Math.abs(tension - previousTension) < 0.02) return;
  previousTension = tension;
  music?.setGameParameter(tensionParameter.id, tension);
}

参数是否有实际听感取决于 pack 的 dspTarget。dspTarget: "none" 可用于游戏状态记录,但不会改变音频。生成合同会带上公开参数的 min、max、defaultValue 和 unit;游戏 UI 与调试工具应复用这些值,不要另抄一份范围。

7. 普通接入不需要额外预加载

loadPulseGame() 返回时已经完成整个包的音频解码,推荐路径不需要再调用预加载方法。

只有使用 createPulseGameRuntime() / createPulseGameRuntimeFromPackage() 的自定义加载流程,才可能需要在自己的加载阶段按公开 state 提前准备资源:

async function prepareCombat() {
  await music?.preloadGameState("combat");
}

preloadGameState() 会对未知或未公开的 state ID reject。PulseGameRuntime 不暴露按 resource ID 预载;确实需要底层资源诊断的自定义工具应明确使用 AudioCore,不要把这条依赖带进普通游戏代码。

8. 生命周期与事件

一个场景、关卡或音乐所有者通常只创建一个 runtime。不要在每次游戏事件时重新下载和创建。

const offs: Array<() => void> = [];

function attachMusicDebug(music: PulseGameRuntime) {
  offs.push(
    music.on("state-change", ({ fromStateId, toStateId }) => {
      console.debug("[music] state", fromStateId, "->", toStateId);
    }),
    music.on("transition-executed", ({ toStateId, mode }) => {
      console.debug("[music] transition", mode, "->", toStateId);
    }),
  );
}

function disposeSceneMusic() {
  for (const off of offs.splice(0)) off();
  music?.stop();
  music?.dispose();
  music = null;
}

游戏 facade 只提供 playback-start、playback-stop、state-change、transition-scheduled、transition-executed 和 parameter-change。事件与 runtime.state.parameterValues 都会按接入合同过滤;resource、segment、隐藏参数和底层音频上下文不会出现在普通游戏 API 中。

subscribe() 适合调试 HUD:它只提供精简后的公开 runtime state;生产逻辑更适合订阅明确的 typed event。

9. 可选:数据驱动事件映射

小项目可直接调用 setGameState / setGameParameter。若游戏已有统一事件总线,可使用可选的 mapping helper。Pulse Desktop 帮助页和 buildIntegrationExample() 都会只依据接入合同生成 starter mapping;下面是对应形式:

import { applyGameEvent, type GameEventMapping } from "@clxgame/pulse-game";
import { pulseContract } from "./pulse-contract";

const mappings: GameEventMapping[] = [
  {
    gameEvent: "encounter:start",
    pulseAction: {
      type: "setGameState",
      stateId: pulseContract.states.combat,
    },
  },
  {
    gameEvent: "encounter:end",
    pulseAction: {
      type: "setGameState",
      stateId: pulseContract.states.exploration,
    },
  },
  {
    gameEvent: "player:danger",
    pulseAction: {
      type: "setGameParameter",
      parameterId: pulseContract.parameters.tension.id,
      value: 0.8,
    },
  },
];

gameEvents.on((eventName) => {
  if (music) applyGameEvent(music, mappings, eventName);
});

未匹配事件是安全的 no-op。示例里的游戏事件名只是占位符,应替换成项目自己的事件名;真正受合同约束的是 pulseAction 里的 state/parameter ID。这个 helper 不携带 transition mode;需要 nextBar、nextEnd 等特殊规则时,直接调用 setGameState。

如果在自定义工具中需要生成同样的合同和示例:

import {
  buildIntegrationExample,
  buildPulseContractSnippet,
} from "@clxgame/pulse-game";

const generated = buildIntegrationExample(module, {
  packageUrl: "/packs/forest-combat.pulse",
});

console.log(generated.contract);        // 结构化公开合同
console.log(generated.contractSnippet); // 可复制的 pulseContract.ts
console.log(generated.snippet);         // 最小加载与事件示例
console.warn(generated.warnings);       // 包括旧包推断警告

// 只需要 TypeScript 合同时:
console.log(buildPulseContractSnippet(module));

10. 静态部署、CDN 与缓存

正式 Web 部署不依赖开发期 /__pulse-sync/* 路由。把 .pulse 一并发布到静态站点或 CDN,并从实际 URL fetch。

  • 推荐使用带版本或 hash 的文件名,例如 /packs/forest-combat.v3.pulse;
  • 更新内容时同步更新游戏配置,避免 CDN 长缓存拿到旧包;
  • 跨域 CDN 需要允许游戏域名的 CORS;
  • 保持二进制原样传输,不要把 .pulse 当 JSON 转码;
  • 建议为下载失败准备静态 BGM 或无音乐回退。

仓库的 HTML5 示例在 production 下加载 /packs/forest-combat.pulse;只有 pnpm dev:game-live 的开发服务器提供 live-sync 路由。

11. 进阶:读取 manifest 或使用自定义 loader

只在需要读取 license/兼容性 metadata,或音频由游戏自己的资源系统管理时,才安装 core:

pnpm add @clxgame/pulse-game @clxgame/pulse-engine

读取 metadata:

import { loadPulsePackage } from "@clxgame/pulse-engine";

const pack = loadPulsePackage(packageBytes, { validate: true });
console.log(pack.manifest.metadata?.license);
console.log(pack.manifest.metadata?.compatibility);
console.log(pack.manifest.metadata?.publicInterface);

若资源不内嵌在 .pulse 中,而由游戏资源系统提供:

import { createPulseGameRuntime } from "@clxgame/pulse-game";
import {
  loadPulsePackage,
  type Resource,
  type ResourceLoader,
} from "@clxgame/pulse-engine";

class GameAssetLoader implements ResourceLoader {
  load(resource: Resource): Promise<ArrayBuffer> {
    return gameAssets.loadArrayBuffer(resource.src);
  }
}

const pack = loadPulsePackage(packageBytes, { validate: true });
const music = createPulseGameRuntime(pack.module, {
  loader: new GameAssetLoader(),
});

metadata.compatibility 目前是交付提示,不会自动阻止 runtime。游戏应在下载/安装阶段决定是否接受 license、codec、最低版本和 feature 要求。不要绕过 PulseGameRuntime 直接用 publicInterface 调底层内部状态;游戏 facade 才负责执行公开边界。

12. 完整 HTML5 联调 Host

仓库内的 HTML5 项目包含合同校验、调试 UI 和 Desktop Live Sync,因此是进阶联调参考,不是应该整段复制的最小代码:

pnpm dev:html5-game

它验证真实 .pulse 和完整生命周期:

download -> strict package load -> ready -> user gesture play
-> setGameState -> setGameParameter -> dispose

为了支持 Live Sync,这个 Host 会读取底层 package 合同并使用低阶 factory。普通游戏仍应使用 loadPulseGame();关键实现见 examples/html5-game/src/main.ts。

Live Sync 属于交付检查,因此比普通 runtime 的旧包兼容更严格:没有经过设计师保存确认的旧包合同会被阻止同步,而不是把推断出的全部内部 ID 直接交给游戏。

13. 上线前检查清单

  • 最终 .pulse 已声明接入合同,帮助页生成的 pulseContract 与交付文件来自同一版本;
  • 若是旧包推断合同,已让内容方逐项确认全部公开 ID,没有忽略警告;
  • loadPulseGame() 从最终部署 URL 成功返回;
  • play() 只在用户手势后调用;
  • 所有 setGameState / setGameParameter 都使用生成合同里的公开 ID;
  • 参数按业务含义归一化并节流,未知 ID 能在开发期暴露;
  • 开发环境记录了加载异常和 transition-executed;
  • 加载/播放失败有可接受的降级;
  • 场景卸载调用 stop()、取消事件监听并 dispose();
  • happy path 从拿到 .pulse 到听到声音不超过 5 分钟。

14. 当前边界

当前可作为生产接入承诺的是 Web Audio / HTML5 host。以下内容不应作为当前包的承诺写入游戏计划:

  • Unity、Godot、Unreal 的正式 runtime 或插件;
  • 独立 stinger/trigger 模型;
  • Wwise/FMOD 级复杂中间件、节点图或完整 DAW;
  • runtime 内置订单、授权或 entitlement 校验。

如果接入过程中遇到 state、parameter、资源加载或部署问题,优先记录:pack 版本、state/parameter 合同、loadPulseGame() 的错误、浏览器网络请求和 music.state 快照。这些信息足以定位绝大多数接入故障。

本页目录

  • 1. 你会拿到什么
  • 2. 安装
  • 3. 放置与加载 `.pulse`
  • 4. 从用户手势开始播放
  • 5. 游戏状态映射
  • 6. 参数映射
  • 7. 普通接入不需要额外预加载
  • 8. 生命周期与事件
  • 9. 可选:数据驱动事件映射
  • 10. 静态部署、CDN 与缓存
  • 11. 进阶:读取 manifest 或使用自定义 loader
  • 12. 完整 HTML5 联调 Host
  • 13. 上线前检查清单
  • 14. 当前边界