@overworld-engine/audio
BGM/音效管理器:场景-曲目映射、切曲淡入淡出与自动播放策略处理
BGM / 音效管理器:单例 BGM 音源、切曲淡入淡出、浏览器自动播放策略处理
(首次用户交互后自动重试)、可选的音量与静音设置持久化(经 persist 显式开启),
并可订阅事件总线上的 scene:changed 事件自动切换场景 BGM。播放经可注入的
AudioBackend(缺省 HTMLAudio,行为不变;微信小游戏注入 weapp 后端)。
包内零游戏内容:曲目表、场景映射全部由配置注入。所有浏览器 API 均有守卫, 在 Node / SSR / 测试环境中导入和创建不会崩溃(仅记录状态、不实际播放)。
快速开始
import { createAudioManager } from '@overworld-engine/audio'
const audio = createAudioManager({
tracks: {
town: '/bgm/town.mp3',
dungeon: '/bgm/dungeon.mp3',
pickup: '/sfx/pickup.mp3',
},
sceneTracks: { plaza: 'town', crypt: 'dungeon' }, // 场景 id → 曲目 id
})
// 之后每次 gameEvents.emit('scene:changed', ...) 都会自动切换 BGM
audio.playSfx('pickup') // 一次性音效配置(AudioManagerConfig)
| 选项 | 默认 | 说明 |
|---|---|---|
tracks | 必填 | 曲目 id → 音频 URL(BGM 与音效共用) |
sceneTracks | — | 场景 id → 曲目 id;未映射的场景会停止当前 BGM |
autoSubscribeSceneChanges | true | 订阅总线 scene:changed 自动换曲 |
events | 全局 gameEvents | 自定义事件总线(测试时传入新实例);bus 是保留的旧别名,两者同传时 events 优先 |
volume / sfxVolume | 0.7 | 初始 BGM / 音效音量(0–1) |
fadeDuration | 1000 | 切曲淡入淡出时长(ms),0 表示立即切换 |
loop | true | BGM 是否循环 |
backend | htmlAudioBackend | 播放后端注入(见下文"后端注入");缺省为 HTMLAudio,行为不变 |
pauseOnHide | false | 订阅总线 app:paused / app:resumed(由 @overworld-engine/platform 桥发出):切后台自动暂停 BGM、回前台恢复;dispose() 退订 |
persist | 省略(关闭) | 框架统一约定:省略或 false 不持久化;true 用默认配置持久化音量/静音(键 overworld:audio);可传对象自定义。v0.9 起省略即关闭(此前默认开启),依赖持久化请显式传 persist: true |
Manager API
-
store— 底层 zustand vanilla store(StoreApi<AudioState>),状态为{ volume, sfxVolume, muted, currentTrackId, unlocked };getState()取快照, React 里用 zustand 的useStore:import { useStore } from 'zustand' const muted = useStore(audio.store, (s) => s.muted) -
playTrack(trackId)/stopTrack()— 播放 / 停止 BGM(带淡入淡出) -
playSceneTrack(sceneId)— 按场景映射播放;resolveSceneTrack(sceneId)仅做解析 -
playSfx(trackId)— 一次性音效 -
setVolume(v)/setSfxVolume(v)— 音量(自动钳制到 0–1) -
setMuted(muted)/toggleMute()— 静音;静音期间仍记录目标曲目,取消静音后恢复播放 -
dispose()— 退订总线、移除解锁监听并停止播放
后端注入(AudioBackend)
管理器的全部播放操作都经过可注入的后端接口,跨端只需换后端、逻辑零改动:
interface AudioBackend {
create(url: string): AudioHandle // 每个音源一个 handle
isAvailable?(): boolean // false = 仅记录状态(Node/SSR 守卫)
}
interface AudioHandle {
play(): Promise<void> | void
pause(): void
setVolume(v: number): void
getVolume(): number // 淡入淡出读取当前音量
setLoop(loop: boolean): void
isPaused(): boolean
onEnded(cb: () => void): () => void // 一次性音效播完自动 destroy
destroy(): void
}- 缺省
htmlAudioBackend(已导出):new Audio(url),与历史行为一致; - 微信小游戏:
backend: createWeappAudioBackend()(来自@overworld-engine/adapters-weapp,映射wx.createInnerAudioContext)。 silentBackend是无 DOM、SSR 与测试环境的静默实现,满足相同契约但不播放声音。
环境音区的纯函数也公开导出:zoneWeight(zone, listener) 根据 inner/outer
radius 返回 0..1 线性权重;mixBuses(...) 把 zone、ambience 与 master 音量
合成为最终 gain。两者无 Web Audio 依赖,可单独测试。
自动播放策略
浏览器通常禁止无交互时自动播放。当 audio.play() 被拒绝时,管理器会在
window 上注册一次性的 pointerdown / keydown 监听,首次用户交互后自动
重试当前曲目;成功后 unlocked 置为 true。
依赖
依赖 @overworld-engine/core(事件总线与持久化辅助);peerDependency 为 zustand。