主题切换
音频系统
预计阅读 15 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+ · 建议在实际调用前使用 `wx.canIUse('createWebAudioContext')` 做兼容性判断
前端对比:Web 音频 vs 小游戏音频
| 维度 | Web <audio> | 微信小游戏音频 |
|---|---|---|
| 背景音乐 | .loop = true 直接播放 | wx.createInnerAudioContext() 单独管理 |
| 短音效 | 可复用同一个 <audio> 实例 | 多个 InnerAudioContext 并发(上限 10 个) |
| 自动播放 | Chrome 限制需用户手势 | iOS 同样限制,需首次触摸解锁 |
| 格式支持 | MP3 / OGG / WAV 通用 | 推荐 MP3(全平台),OGG 部分支持 |
| 高级音效 | Web Audio API(AudioContext) | 微信 WebAudio 支持基础节点 |
适应技巧: Web 前端的音频常被视为"附加功能",但在游戏中音频是核心体验的一部分。背景音乐、跳跃音效、得分音效、死亡音效——每个都有独立的播放逻辑和音量管理需求。建议从一开始就设计完整的音频架构(如本章的 BGMManager + SoundEffectManager 双通道模式),而不是后期临补。
音频是游戏的灵魂。本章讲解微信小游戏中的音频播放、管理与最佳实践。
音频 API 选择
微信小游戏提供两种音频播放方式:
| API | 适用场景 | 特点 |
|---|---|---|
wx.createInnerAudioContext() | 背景音乐、长音频 | 自动管理音频解码、缓存,支持系统音量通道 |
WebAudio API | 音效、实时音频处理 | 低延迟、多实例、支持音频节点图,但内存占用较高 |
推荐策略
- 背景音乐:使用
wx.createInnerAudioContext(),支持系统音量通道、锁屏控制。 - 短音效(射击、点击、碰撞):使用
WebAudio API,低延迟、多实例并发。 - 语音/长音频:使用
wx.createInnerAudioContext()。
各引擎音频加载方式对比
不同引擎封装了各自的音频 API,但底层都基于 wx.createInnerAudioContext() 或 WebAudio:
Cocos Creator 内置 AudioSource 组件,支持拖拽配置:
typescript
import { AudioSource, resources } from 'cc'
// 播放背景音乐
resources.load('audio/bgm', (err, clip) => {
if (err) {
console.error('加载失败', err)
return
}
const audioSource = this.node.getComponent(AudioSource)
audioSource.clip = clip
audioSource.loop = true
audioSource.play()
})
// 播放短音效(Cocos 3.x 内置音效池)
import { AudioClip, AudioSource } from 'cc'
// 引擎自动管理音效实例池,无需手动创建多个 InnerAudioContextInnerAudioContext(背景音乐)
基础使用
typescript
class BGMManager {
private bgm: WechatMinigame.InnerAudioContext
private _isPlaying = false
private _errorCount = 0
private _retryTimer = -1
private readonly _maxRetries = 3
constructor() {
this.bgm = wx.createInnerAudioContext()
this.bgm.src = 'https://cdn.example.com/bgm.mp3'
this.bgm.loop = true
this.bgm.volume = 0.6
this.bgm.autoplay = false
// 自然播放结束
this.bgm.onEnded(() => {
console.log('BGM 播放完成')
this._errorCount = 0
})
// 音频错误处理:带退避与重试上限
this.bgm.onError((err) => {
console.error('BGM 播放错误', err)
if (this._errorCount >= this._maxRetries) {
console.warn('BGM 重试次数已达上限,停止播放')
return
}
this._errorCount++
clearTimeout(this._retryTimer)
this._retryTimer = setTimeout(() => this.play(), 1000 * this._errorCount)
})
}
play(): void {
this.bgm.play()
this._isPlaying = true
}
pause(): void {
this.bgm.pause()
this._isPlaying = false
}
stop(): void {
this.bgm.stop()
this._isPlaying = false
}
setVolume(v: number): void {
this.bgm.volume = Math.max(0, Math.min(1, v))
}
get isPlaying(): boolean {
return this._isPlaying
}
/** 销毁 BGM 资源:页面/场景切换时调用 */
destroy(): void {
clearTimeout(this._retryTimer)
this.bgm.stop()
this.bgm.destroy()
this._isPlaying = false
}
}iOS 自动播放限制
iOS 下音频必须在用户交互事件回调中首次播放,否则会被阻塞。首次激活后应及时移除监听并释放静音文件实例:
typescript
// 在游戏第一个触摸事件中激活音频
let audioActivated = false
let unlockAudio: (() => void) | null = null
export function setupIOSAudioUnlock(onUnlocked: () => void) {
unlockAudio = () => {
if (audioActivated || !unlockAudio) return
audioActivated = true
// 用静音文件“解锁”iOS 音频上下文,音量设为 0 避免用户感知
const unlock = wx.createInnerAudioContext()
unlock.src = 'audio/silence.mp3' // 1 秒静音文件
unlock.volume = 0
unlock.play()
unlock.onEnded(() => {
unlock.destroy()
onUnlocked()
})
wx.offTouchStart(unlockAudio)
unlockAudio = null
}
wx.onTouchStart(unlockAudio)
}
// 使用示例:setupIOSAudioUnlock(() => bgmManager.play())WebAudio API(音效系统)
音效管理器
typescript
class SoundEffectManager {
private audioContext: AudioContext | null = null
private buffers: Map<string, AudioBuffer> = new Map()
private loaded = false
constructor() {
// 微信小游戏基础库需支持 createWebAudioContext,低版本应降级到 InnerAudioContext
if (!wx.canIUse('createWebAudioContext')) {
console.warn('当前基础库不支持 WebAudio,请使用 InnerAudioContext 播放音效')
return
}
this.audioContext = wx.createWebAudioContext()
}
/** 预加载音效 */
async preload(sounds: Record<string, string>): Promise<void> {
if (!this.audioContext) {
console.warn('WebAudio 不可用,跳过预加载')
return
}
const entries = Object.entries(sounds)
await Promise.all(
entries.map(async ([name, url]) => {
try {
const arrayBuffer = await this.fetchAudio(url)
const audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer)
this.buffers.set(name, audioBuffer)
} catch (err) {
console.error(`音效 ${name} 加载失败`, err)
}
})
)
this.loaded = true
console.log(`预加载 ${this.buffers.size}/${entries.length} 个音效完成`)
}
/** 播放音效 */
play(name: string, volume = 1, loop = false): void {
if (!this.audioContext) {
console.warn('WebAudio 不可用,无法播放音效')
return
}
const buffer = this.buffers.get(name)
if (!buffer) {
console.warn(`音效 "${name}" 未加载`)
return
}
// 每次播放创建新的 BufferSource(可多实例并发)
const source = this.audioContext.createBufferSource()
source.buffer = buffer
source.loop = loop
// 音量控制节点
const gainNode = this.audioContext.createGain()
gainNode.gain.value = Math.max(0, Math.min(1, volume))
source.connect(gainNode)
gainNode.connect(this.audioContext.destination)
source.start(0)
// 播放结束后自动销毁
source.onended = () => {
source.disconnect()
gainNode.disconnect()
}
}
/** 从网络加载音频资源 */
private async fetchAudio(url: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
wx.request({
url,
responseType: 'arraybuffer',
success: (res) => resolve(res.data as ArrayBuffer),
fail: reject,
})
})
}
/** 销毁音频上下文,释放资源 */
destroy(): void {
this.buffers.clear()
this.audioContext?.close?.()
this.audioContext = null
this.loaded = false
}
}WebAudio 兼容性
wx.createWebAudioContext 需要较高版本的基础库支持。在真机上应先通过 wx.canIUse('createWebAudioContext') 检测,否则降级到 InnerAudioContext。即使支持 WebAudio,也应保留 InnerAudioContext 作为背景音乐方案,因为后者在后台播放、锁屏控制等方面更成熟。
使用示例
typescript
// 使用示例
const sfx = new SoundEffectManager()
await sfx.preload({
shoot: 'https://cdn.example.com/shoot.wav',
explosion: 'https://cdn.example.com/explosion.wav',
coin: 'https://cdn.example.com/coin.wav',
})
sfx.play('shoot', 0.8)
sfx.play('coin', 1.0)
sfx.destroy() // 场景切换时释放资源音频格式选择
格式建议
| 格式 | 压缩率 | 音质 | 适用场景 |
|---|---|---|---|
| mp3 | 较高 | 良好 | 背景音乐(兼容性最佳) |
| aac / m4a | 高 | 优于 mp3 | iOS 更友好,小体积 |
| wav | 无损 | 最佳 | 短音效(解码快、延迟低) |
| ogg | 高 | 良好 | Android 友好 |
建议: 背景音乐使用 mp3(比特率 ≤ 128kbps),短音效使用 wav(码率更低时用 ogg/mp3)。
AI 生成音频资源
对于没有音频制作经验的开发者,AI 工具可以快速获得可替换的音频原型:
| 类型 | 推荐工具 | 注意 |
|---|---|---|
| BGM 原型 | Suno、Udio、Stable Audio | 免费版通常不可商用;生成后需处理循环点 |
| 音效 | ElevenLabs Sound Effects、Google SoundFX | 导出后转为单声道,控制体积 |
| 配音/TTS | ElevenLabs、Fish Audio、讯飞配音 | 注意 iOS 静音开关限制 |
导入微信前需完成:
- 格式转换:用 ffmpeg 转为 MP3/WAV
- 循环处理:BGM 需找到无缝循环点
- 体积控制:BGM ≤ 1MB,音效 ≤ 100KB
- 授权确认:核心 BGM 建议用授权音乐库,避免版权风险
示例:把 AI 生成的 WAV 转为小游戏可用 MP3
bash
# BGM:128kbps 立体声
ffmpeg -i ai-bgm.wav -ar 44100 -ac 2 -b:a 128k assets/bgm.mp3
# 音效:22kHz 单声道
ffmpeg -i ai-sfx.wav -ar 22050 -ac 1 -b:a 64k assets/sfx.mp3静音与音量管理
游戏中的音量通常分为"背景音乐"和"音效"两个独立通道:
typescript
class AudioSettings {
private static STORAGE_KEY = 'audioSettings'
masterVolume = 1
bgmVolume = 1
sfxVolume = 1
isMuted = false
// 从本地缓存恢复设置
static load(): AudioSettings {
try {
const raw = wx.getStorageSync(this.STORAGE_KEY)
return raw ? Object.assign(new AudioSettings(), JSON.parse(raw)) : new AudioSettings()
} catch {
return new AudioSettings()
}
}
save(): void {
wx.setStorageSync(
AudioSettings.STORAGE_KEY,
JSON.stringify({
masterVolume: this.masterVolume,
bgmVolume: this.bgmVolume,
sfxVolume: this.sfxVolume,
isMuted: this.isMuted,
})
)
}
toggleMute(): void {
this.isMuted = !this.isMuted
this.apply()
this.save()
}
apply(bgm?: BGMManager): void {
const master = this.isMuted ? 0 : this.masterVolume
bgm?.setVolume(master * this.bgmVolume)
// SFX 管理器也应在 play 时乘以 master * sfxVolume
}
}微信音频系统限制
- 并发音频数限制:
InnerAudioContext实例建议不超过 10 个。 - 音频文件大小:单音频文件建议 < 5MB,多个大文件需考虑分包或 CDN 加载。
- 后台播放:iOS 锁屏或切换到后台后音频可能被暂停,需在
wx.onShow/wx.onHide中处理恢复/暂停逻辑。 - 静音模式下播放:可调用
wx.setInnerAudioOption({ obeyMuteSwitch: false })使手机处于静音开关时仍能播放音频。但非必要音频(如 BGM、普通音效)开启此项可能被审核以“影响用户体验”为由驳回,建议仅在核心玩法必需音频场景使用,并准备合规说明。
3D/空间音频
空间音频(Spatial Audio)让声音具有方向感和距离感——脚步声从左前方传来,爆炸声由远及近。在微信小游戏中,通过 WebAudio API 的 PannerNode 可以实现基础的 3D 音效。
前提条件
空间音频依赖 WebAudio API,需要先检测兼容性:
typescript
function canUseSpatialAudio(): boolean {
return wx.canIUse('createWebAudioContext')
}PannerNode 实现
typescript
// audio/SpatialAudioManager.ts
/**
* 3D 空间音频管理器
* 使用 WebAudio PannerNode 实现声音的空间定位
*/
class SpatialAudioManager {
private audioCtx: wx.WebAudioContext | null = null
private listener: wx.AudioListener | null = null
private activeSources: Map<
string,
{
source: wx.AudioBufferSourceNode
panner: wx.PannerNode
gain: wx.GainNode
}
> = new Map()
constructor() {
if (!wx.canIUse('createWebAudioContext')) {
console.warn('当前环境不支持 WebAudio,空间音频不可用')
return
}
this.audioCtx = wx.createWebAudioContext()
this.listener = this.audioCtx.listener
// 设置监听器(代表玩家的"耳朵")的初始位置为原点
if (this.listener.positionX) {
this.listener.positionX.value = 0
this.listener.positionY.value = 0
this.listener.positionZ.value = 0
}
}
/**
* 播放一个位于 3D 空间中的音效
* @param sourceX - 声源 X 坐标(-1 到 1,-1 = 左,1 = 右)
* @param sourceY - 声源 Y 坐标(0 = 相同高度)
* @param sourceZ - 声源 Z 坐标(正值 = 前方,负值 = 后方)
*/
play3DSound(buffer: wx.AudioBuffer, sourceX: number, sourceY: number, sourceZ: number): void {
if (!this.audioCtx) return
const source = this.audioCtx.createBufferSource()
source.buffer = buffer
// 创建 PannerNode(3D 定位器)
const panner = this.audioCtx.createPanner()
panner.setPosition(sourceX, sourceY, sourceZ)
// 设置声源朝向(默认全向)
panner.setOrientation(0, 0, -1)
// 距离衰减模型:inverse(距离越远音量越小)
panner.distanceModel = 'inverse'
panner.refDistance = 1 // 参考距离
panner.maxDistance = 100 // 最大可听距离
// GainNode 用于额外的音量控制
const gain = this.audioCtx.createGain()
gain.gain.value = 1.0
// 连接:source → panner → gain → destination
source.connect(panner)
panner.connect(gain)
gain.connect(this.audioCtx.destination)
const id = `sfx_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`
this.activeSources.set(id, { source, panner, gain })
source.onended = () => {
this.activeSources.delete(id)
}
source.start()
}
/**
* 更新监听器位置(应在游戏主循环中调用)
* @param x - 玩家/摄像机 X 坐标
* @param y - 玩家/摄像机 Y 坐标
*/
updateListenerPosition(x: number, y: number): void {
if (!this.listener?.positionX) return
// 将游戏世界坐标映射到音频空间
// 可根据游戏比例调整映射系数
const audioX = x / 100
const audioY = y / 100
this.listener.positionX.value = audioX
this.listener.positionY.value = audioY
this.listener.positionZ.value = 0
}
/** 释放所有活跃音源 */
stopAll(): void {
this.activeSources.forEach(({ source }) => {
try {
source.stop()
} catch {
/* 可能已结束 */
}
})
this.activeSources.clear()
}
/** 销毁空间音频系统 */
destroy(): void {
this.stopAll()
this.audioCtx?.close()
this.audioCtx = null
}
}使用场景
| 场景 | 空间音频效果 |
|---|---|
| 横版动作游戏 | 敌人脚步声从左侧/右侧传来 |
| 俯视角射击 | 枪声距离感应(远小近大) |
| 3D 赛车 | 引擎声多普勒效应(需要额外的频率偏移计算) |
| 休闲游戏 | 通常不需要——空间音频增加实现复杂度,对休闲游戏体验提升有限 |
性能与兼容性
- 性能开销:每个
PannerNode都有独立的实时位置计算,同时播放过多 3D 音效(>10 个)可能导致性能下降。建议使用音频池限制并发数。 - 兼容性:部分旧版微信客户端(基础库 < 2.10.0)不支持 WebAudio API,请使用
wx.canIUse('createWebAudioContext')检测并降级为 InnerAudioContext 普通播放。 - iOS 限制:iOS 上的 WebAudio 同样受自动播放限制,需在用户首次触摸后
resume()AudioContext。
音频预加载与进度反馈
网络不佳时,音频文件可能需要数秒才能开始播放。提供预加载和进度反馈能显著提升用户体验。
预加载管理器
typescript
// audio/AudioPreloader.ts
interface AudioAsset {
key: string // 资源标识符
src: string // 音频 URL 或本地路径
type: 'bgm' | 'sfx'
}
interface LoadProgress {
loaded: number // 已加载(字节)
total: number // 总大小(字节)
percent: number // 进度百分比
}
class AudioPreloader {
private audioMap: Map<string, wx.InnerAudioContext> = new Map()
private loadingSet: Set<string> = new Set()
private onProgress?: (key: string, progress: LoadProgress) => void
private onComplete?: (key: string) => void
private onError?: (key: string, err: Error) => void
/**
* 预加载音频资源列表
* @param assets 音频资源清单
* @param callbacks 进度回调
*/
async preloadAll(
assets: AudioAsset[],
callbacks?: {
onProgress?: (key: string, progress: LoadProgress) => void
onComplete?: (key: string) => void
onError?: (key: string, err: Error) => void
}
): Promise<void> {
this.onProgress = callbacks?.onProgress
this.onComplete = callbacks?.onComplete
this.onError = callbacks?.onError
// 并行加载,但可以通过信号量控制并发数
const MAX_CONCURRENT = 3
const queue = [...assets]
const active: Promise<void>[] = []
while (queue.length > 0 || active.length > 0) {
while (active.length < MAX_CONCURRENT && queue.length > 0) {
const asset = queue.shift()!
const promise = this.loadSingle(asset).then(() => {
active.splice(active.indexOf(promise), 1)
})
active.push(promise)
}
if (active.length > 0) {
await Promise.race(active)
}
}
}
private loadSingle(asset: AudioAsset): Promise<void> {
return new Promise((resolve, reject) => {
if (this.loadingSet.has(asset.key)) {
resolve()
return
}
this.loadingSet.add(asset.key)
const audio = wx.createInnerAudioContext()
audio.src = asset.src
// 对于 BGM,设置循环
if (asset.type === 'bgm') {
audio.loop = true
}
// 监听 canplay 事件表示可以开始播放(无需等待全部下载完成)
audio.onCanplay(() => {
this.audioMap.set(asset.key, audio)
this.loadingSet.delete(asset.key)
this.onComplete?.(asset.key)
resolve()
})
audio.onError((err: any) => {
this.loadingSet.delete(asset.key)
const error = new Error(`音频加载失败: ${asset.key} — ${err.errMsg || 'Unknown'}`)
this.onError?.(asset.key, error)
reject(error)
})
})
}
/** 获取已加载的音频实例 */
getAudio(key: string): wx.InnerAudioContext | undefined {
return this.audioMap.get(key)
}
/** 检查某个音频是否已加载完成 */
isLoaded(key: string): boolean {
return this.audioMap.has(key)
}
/** 加载进度百分比(粗略估计——基于资源数量而非字节) */
getOverallProgress(assetCount: number): number {
return assetCount > 0 ? this.audioMap.size / assetCount : 0
}
/** 销毁所有预加载的音频实例 */
destroy(): void {
this.audioMap.forEach((audio) => audio.destroy())
this.audioMap.clear()
this.loadingSet.clear()
}
}加载进度 UI 示例
typescript
// 在游戏加载场景中使用
async function showLoadingScreen(): Promise<void> {
const preloader = new AudioPreloader()
const assets: AudioAsset[] = [
{ key: 'bgm_main', src: 'audio/bgm.mp3', type: 'bgm' },
{ key: 'sfx_flap', src: 'audio/flap.mp3', type: 'sfx' },
{ key: 'sfx_score', src: 'audio/score.mp3', type: 'sfx' },
{ key: 'sfx_die', src: 'audio/die.mp3', type: 'sfx' },
]
let loadedCount = 0
await preloader.preloadAll(assets, {
onComplete: (key) => {
loadedCount++
const percent = Math.round((loadedCount / assets.length) * 100)
console.log(`音频加载进度: ${percent}% (${key} 完成)`)
// 更新加载界面进度条
updateLoadingBar(percent)
},
onError: (key, err) => {
console.warn(`音频 ${key} 加载失败,将跳过`, err)
// 失败不影响游戏启动——静默降级
},
})
console.log('音频预加载完成')
}预加载最佳实践
- BGM 必预加载:背景音乐文件最大,预加载避免游戏开始后无声。
- 不阻塞启动:音效加载失败不应阻止游戏启动——静默降级,无音频也能玩。
- 按需加载:大型游戏按关卡分包预加载,而不是一次性加载所有音频。
- 并发控制:限制同时下载的音频数量(建议 ≤ 3),避免抢占游戏主资源的带宽。
- 超时处理:
onCanplay可能永远不会触发(网络问题),应设置 10 秒超时并降级。
📝 课后练习
练习 1:音频管理器设计
题目: 你的游戏需要同时播放背景音乐和 5 种音效(跳跃、得分、碰撞、按钮、胜利)。设计一个音频管理器的类结构,支持全局静音和音量控制。
参考答案:
typescript
class AudioManager {
private bgm: wx.InnerAudioContext
private sfxPool: wx.InnerAudioContext[] = []
private masterVolume = 1
private bgmVolume = 0.7
private sfxVolume = 1
private muted = false
playBGM(src: string) {
/* 暂停当前 BGM,创建新的并播放 */
}
playSFX(src: string) {
/* 从池中取空闲实例或创建新的(上限 10 个)*/
}
setMasterVolume(v: number) {
this.masterVolume = v
this.applyVolumes()
}
toggleMute() {
this.muted = !this.muted
this.applyVolumes()
}
private applyVolumes() {
const master = this.muted ? 0 : this.masterVolume
this.bgm.volume = master * this.bgmVolume
this.sfxPool.forEach((sfx) => (sfx.volume = master * this.sfxVolume))
}
}练习 2:iOS 自动播放限制
题目: iOS 设备限制音频必须在用户手势中首次播放。写一段代码处理这个限制。
参考答案:
javascript
// 在首次触摸事件中解锁音频
wx.onTouchStart(function unlockAudio() {
const audio = wx.createInnerAudioContext()
audio.src = 'audio/silence.mp3' // 1 秒静音文件
audio.volume = 0
audio.play()
audio.onEnded(() => audio.destroy())
wx.offTouchStart(unlockAudio) // 只执行一次
})