音频
Estella 的音频通过 Audio 资源播放一次性音效和循环音乐,用句柄控制单个声音,用总线层级
平衡混音,并用空间音频把声音定位到世界里。它在浏览器上跑 Web Audio 后端,在微信上跑原生后端。
Audio 是逐 App 的资源,默认已注册。音频文件会被解码成缓冲区并在首次使用时缓存,然后
在某个总线(命名的音量分组)上播放。每次播放返回一个 AudioHandle,你可以留着它来调节
或停止这一个声音。对于绑定到某个位置或物体的声音,AudioSource 组件声明式地播放,并在
(spatial)开启时按与 AudioListener 的距离衰减。
解码后的缓冲区放在一个按字节预算的热缓存里(默认 32 MB,
RuntimeConfig.audioCacheBudget,构建配置里为 audioCacheBudget)——纹理预算的音频镜像:
被资源生命周期释放的缓冲区仍可即时播放,再次加载时原地复活而不用重新下载和解码;超出预算时
最旧的未使用缓冲区被丢弃。收到系统内存警告时引擎会自动清空整个热缓存——正在播放的声音不受
影响。
import { defineSystem, Res, Audio } from 'esengine';
const play = defineSystem([Res(Audio)], (audio) => { const sfx = audio.playSFX('assets/hit.wav', { volume: 1, pitch: 1, pan: 0 }); audio.playBGM('assets/theme.ogg', { fadeIn: 0.5, crossFade: 1.0 });});playSFX 触发一次性播放并返回一个句柄;playBGM 循环并替换当前曲目,可选交叉淡入。两者都会
自动预加载未缓存的片段并在就绪后播放。
Audio API 参考
Section titled “Audio API 参考”| 方法 | 说明 |
|---|---|
playSFX(url, config?) |
播放一次性;返回 AudioHandle。config:volume、pitch、pan、priority。 |
playBGM(url, config?) |
循环音乐,替换当前曲目。config:volume、fadeIn、crossFade。 |
stopBGM(fadeOut?) |
停止音乐,可在 fadeOut 秒内淡出。 |
stopAll() |
停止所有正在播放的声音。 |
setMasterVolume(v) |
设置 master 总线音量(0..1)。 |
setMusicVolume(v) |
设置 music 总线音量。 |
setSFXVolume(v) |
设置 sfx 总线音量。 |
setUIVolume(v) |
设置 ui 总线音量。 |
muteBus(name, muted) |
按名字静音/取消静音一个总线。 |
preload(url) |
提前解码并缓存一个片段(异步)。 |
preloadAll(urls) |
预加载一组片段(异步)。 |
getBufferHandle(url) |
url 的已缓存缓冲区,或 undefined。 |
getSpectrum(out) |
用主输出的频谱填充一个 Uint8Array(每个频段 0–255,从低频到高频),供可视化用。在没有分析能力的后端(微信)上返回 false——按静音处理。 |
setBufferBudget(bytes) |
覆盖热缓存字节预算(null 回到 RuntimeConfig 的值;0 关闭缓存)。 |
getBufferStats() |
驻留计数:bufferCount、bufferBytes、bufferBudget、evictableCount。 |
trimBufferCache() |
立即释放所有未使用的缓存缓冲区(引擎在系统内存警告时会自动调用)。 |
控制正在播放的声音
Section titled “控制正在播放的声音”playSFX 返回一个 AudioHandle——留着它驱动这个特定的声音:
const shot = audio.playSFX('assets/laser.wav');shot.setVolume(0.6);shot.setPan(-0.5); // -1 left … +1 rightshot.setPlaybackRate(1.2); // pitch / speedshot.setLoop(true);shot.pause(); shot.resume(); shot.stop();shot.onEnd = () => { /* the sound finished */ };| 成员 | 类型 | 说明 |
|---|---|---|
setVolume(v) |
方法 | 音量,0..1。 |
setPan(p) |
方法 | 立体声声像,-1(左)…+1(右)。 |
setPlaybackRate(r) |
方法 | 播放速度 / 音调(1 = 正常)。 |
setLoop(l) |
方法 | 循环该声音。 |
pause() / resume() / stop() |
方法 | 传输控制。 |
onEnd |
回调 | 声音结束时调用一次。 |
isPlaying |
只读 | 是否正在播放。 |
currentTime |
只读 | 播放位置(秒)。 |
duration |
只读 | 总时长(秒)。 |
声明式播放 —— AudioSource
Section titled “声明式播放 —— AudioSource”给实体加一个 AudioSource 即可不写代码播放——非常适合环境音、循环的机器声,或锚定到物体的
声音。配 playOnAwake + enabled,音频系统会自动开始播放。
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
clip |
asset | '' |
要播放的音频资源。 |
bus |
string | 'sfx' |
路由经过的混音总线。 |
volume |
number | 1 |
音量,0..1。 |
pitch |
number | 1 |
播放速度 / 音调。 |
loop |
boolean | false |
循环片段。 |
playOnAwake |
boolean | false |
实体启用时自动开始。 |
spatial |
boolean | false |
按与监听器的距离衰减。 |
minDistance |
number | 100 |
在此距离内音量为满。 |
maxDistance |
number | 1000 |
超过此距离即静音。 |
attenuationModel |
AttenuationModel |
Inverse |
衰减曲线——Linear / Inverse / Exponential。 |
rolloff |
number | 1 |
衰减陡峭度的倍率。 |
priority |
number | 0 |
池满时的声部优先级(高者胜)。 |
enabled |
boolean | true |
关掉即停止并静音,而不移除组件。 |
混音器是层级的——master → { music, sfx, ui, voice }。设置某个总线的音量会缩放路由到它下面
的一切:
audio.setMasterVolume(0.8);audio.setMusicVolume(0.6);audio.setSFXVolume(1.0);audio.setUIVolume(0.9);audio.muteBus('music', true);用 AudioSource.bus 字段把声音路由到某个总线,或让 playSFX 默认走 sfx。音量设置器有
setMasterVolume(全局滑条)和 setMusicVolume / setSFXVolume / setUIVolume。voice
总线能路由声音但没有音量设置器——用 muteBus('voice', …) 开关它。
总线效果与自动闪避(ducking)
Section titled “总线效果与自动闪避(ducking)”每条总线携带一条 DSP 插入链,以数据声明:
audio.setBusEffects('music', [ { type: 'filter', filter: 'lowpass', frequency: 800, q: 1 }, // 闷化(暂停菜单) { type: 'reverb', seconds: 1.5, wet: 0.3 }, // 程序化房间混响 { type: 'compressor', thresholdDb: -24, ratio: 4 },]);audio.setBusEffects('music', []); // 清空旁链闪避是一条规则,不是代码——语音播放时自动压低音乐:
audio.setBusDucking('music', { trigger: 'voice', amount: 0.3, attack: 0.05, release: 0.4 });audio.setBusDucking('music', null); // 移除trigger 总线有信号时,目标总线的闪避级压到 amount;静默后按 release 恢复到 1。
闪避是独立的增益级,不会与用户音量设置打架。无 WebAudio 图的后端(微信)上,
两个 API 与音量调用一样优雅降级为空操作。
音频混音器面板与项目配置
Section titled “音频混音器面板与项目配置”底部停靠的音频混音器面板可视化编辑以上一切——每总线一条 strip:音量推子、
静音、效果链、闪避规则、自定义总线。编辑持久化到 project.esproject
(features.audio)并在编辑器中即时生效;Play 与所有导出以同一套混音启动。
导入设置与打包音频
Section titled “导入设置与打包音频”选中音频资产会显示解码波形(可播放/点击跳转)和导入设置:Compress 与
Bitrate 控制打包时的 WAV → MP3 转码(在打包对话框勾选压缩音频)。
已压缩格式原样通过。MP3 有少量编码器延迟——需要无缝循环的素材请关闭 Compress。
Audio 是一个门面,其后是 Web Audio 后端组装的三个导出类——AudioMixer、AudioBus、
AudioPool。游戏里很少直接构造它们(微信后端上它们根本不存在,这正是总线 API 在那里
降级为空操作的原因),但它们公开导出是为了两件实事:内建四条之外的自定义总线,以及
独立音频管线——试听工具、自定义 PlatformAudioBackend、测试——建在你自己的
AudioContext 上。
自定义总线完全不需要类——门面就够:
audio.ensureBus('ambience'); // create under master (idempotent)audio.ensureBus('footsteps', 'sfx'); // or under an existing busaudio.setBusVolume('ambience', 0.5);// playSFX 始终走 'sfx' 总线;playTrack(或实体的 AudioSource.bus)才能选总线。audio.playTrack('assets/wind.ogg', { bus: 'ambience' });ensureBus(name, parent?) 在总线缺失时创建它(挂在 parent 下,默认 master),
无混音图的后端上返回 false;之后所有按名字的门面调用(setBusVolume、muteBus、
setBusEffects、setBusDucking)都能触达它。
AudioBus
Section titled “AudioBus”一条混音 strip:input → [效果插入…] → duck → gain(volume) → parent。声源和子总线
连到 input;node 是输出增益——音量/静音作用在这里,父总线(或做 VU 表的
AnalyserNode)也从这里取信号。闪避级是独立的增益,所以旁链闪避永远不会与用户音量设置
打架。用 AudioBusConfig 构造:
AudioBusConfig |
类型 | 默认 | 说明 |
|---|---|---|---|
name |
string | – | 总线名。 |
volume |
number | 1 |
初始音量,钳制到 0..1。 |
muted |
boolean | false |
以静音启动。 |
parent |
string | 'master' |
父总线名(由 AudioMixer.createBus 消费)。 |
| 成员 | 说明 |
|---|---|
input / node |
入口 GainNode / 输出 GainNode(analyser 从 node 取信号)。 |
volume |
0..1;变化在约 15 ms 内平滑,永不爆音。 |
muted |
把输出渐变到 0 再回来,不丢失 volume。 |
effects / setEffects(defs) |
读取 / 幂等替换 DSP 插入链(BusEffectDef[])。 |
duckTo(level, timeConstant) |
把闪避级向 level 渐变(混音器的闪避驱动它)。 |
connect(dest) / addChild(bus) |
把输出接进父总线或裸 AudioNode。 |
AudioMixer
Section titled “AudioMixer”拥有总线树。构造一个就建好接到 context.destination 的
master → { music, sfx, ui, voice },音量来自 AudioMixerConfig
(masterVolume / musicVolume / sfxVolume / uiVolume / voiceVolume——
music 默认 0.8,其余 1):
import { AudioMixer } from 'esengine';
const mixer = new AudioMixer(new AudioContext(), { musicVolume: 0.6 });const ambience = mixer.createBus({ name: 'ambience' }); // under master| 方法 | 说明 |
|---|---|
master / music / sfx / ui / voice |
内建总线,作为只读字段。 |
getBus(name) / busNames() |
查找总线 / 列出所有名字(创建顺序,master 在先)。 |
createBus(config) |
创建总线并接到 config.parent 下(默认 master)。 |
setDucking(target, rule | null) / getDucking(target) |
安装或清除旁链 BusDuckRule(trigger、amount、attack、release、threshold)。 |
updateDucking() |
每帧:测量各触发总线的 RMS,渐变其目标的闪避级(引擎的混音器由音频系统调用)。 |
AudioPool
Section titled “AudioPool”一次性播放背后的声部池:预建的 gain → panner 节点对(PooledAudioNode),每次播放
acquire()、声音结束时 release(),超过初始 16 个时按需增长;activeCount 与
capacity 报告用量。当你在自己的 AudioContext 上做自定义播放、想避免每次播放的节点
开销时可以用它。
设 AudioSource.spatial = true,并给监听实体(通常是相机或玩家)加一个启用的 AudioListener。
声源便按距离衰减:
import { defineSystem, Commands, Transform, AudioSource, AudioListener, AttenuationModel } from 'esengine';
const setupSpatial = defineSystem([Commands()], (cmds) => { // A positioned, looping ambience that fades with distance. cmds.spawn() .insert(Transform, { position: { x: 600, y: 0, z: 0 } }) .insert(AudioSource, { clip: 'assets/campfire.ogg', loop: true, playOnAwake: true, spatial: true, minDistance: 80, maxDistance: 900, attenuationModel: AttenuationModel.Inverse, });
// The listener (attach to the camera/player). cmds.entity(cameraEntity).insert(AudioListener, { enabled: true });});AttenuationModel |
衰减 |
|---|---|
Linear |
从 minDistance 到 maxDistance 的直线。 |
Inverse(默认) |
自然的反距离衰减。 |
Exponential |
更陡的指数衰减。 |
AudioListener 只有一个字段 enabled(默认 true)——场景里保持恰好一个启用的监听器。
Estella 的空间管线是诚实的 2D:每帧,音频系统取声源和监听器的世界位置,算出距离,用
volume × 衰减 加上沿 x 轴的立体声声像((sourceX − listenerX) / maxDistance,
钳制到 ±1)驱动正在播放的句柄。没有 HRTF、多普勒或遮挡。空间声源在没有启用监听器时播放,
会警告一次并从世界原点量距离。
曲线数学以纯函数导出,供工具和测试使用——calculateAttenuation(distance, config)
配 SpatialAudioConfig(model、refDistance、maxDistance、rolloff——系统用
AudioSource.minDistance/maxDistance/rolloff 填充它),以及
calculatePanning(sourceX, sourceY, listenerX, listenerY, maxDistance):
import { calculateAttenuation, calculatePanning, AttenuationModel } from 'esengine';
const gain = calculateAttenuation(250, { model: AttenuationModel.Inverse, refDistance: 100, maxDistance: 1000, rolloff: 1,}); // 0.4 — refDistance / distanceconst pan = calculatePanning(600, 0, 0, 0, 1000); // 0.6 — to the right- 对在关键时刻要播放的片段预加载(
preload/preloadAll),第一次播放就不会因解码而静音。 - 在手势上开始——浏览器在第一次用户输入前会屏蔽音频。
- 分开总线(
music/sfx/ui/voice)让玩家能各自调节,并统一由master驱动。 - 用
AudioSource上的priority限制同时播放的声音数,声部池满时重要的声音才胜出。 - 世界/物体声音用
AudioSource(声明式),一次性的 UI/玩法音效用playSFX。