Skip to content

用 Tone.js 做游戏音频

Tone.js 构建在 Web Audio API 之上,让合成音频变得实用。你不用加载音频文件,而是用程序生成声音。这意味着零加载时间、无限的变化,以及能实时响应玩法的声音。

试一试(点击按钮听合成的游戏音效):

为什么用 Tone.js 而不是音频文件

音频文件适合做音乐和复杂的声音。但对于游戏音效,合成有真正的优势。你可以每次播放都改变音高和时长,让声音不重复。你可以基于游戏状态即时生成声音。而且完全跳过网络请求。

缺点是合成音频比直接播放文件更占 CPU。对简单游戏来说不是问题。对复杂游戏,你可能两种方式混着用。

安装

从 CDN 或 npm 引入 Tone.js:

html
<script src="https://unpkg.com/tone@15/build/Tone.js"></script>

或用 npm:

bash
npm install tone
js
import * as Tone from 'tone'

自动播放问题

浏览器在用户交互前阻止音频播放。Tone.js 会处理这件事,但你需要在用户手势里调用 Tone.start()

js
document.addEventListener('click', async () => {
  await Tone.start()
  console.log('Audio ready')
}, { once: true })

不写这个,AudioContext 会一直挂起,什么都不会播。

你的第一个合成器

合成器生成声音。最简单的是 Tone.Synth

js
const synth = new Tone.Synth().toDestination()

// 播一个音
synth.triggerAttackRelease('C4', '8n')

triggerAttackRelease 按时长播放一个音符。'C4' 是中央 C。'8n' 在当前节奏下是八分音符。

合成器包含一个振荡器(声源)和一个包络(声音如何淡入淡出)。两者都可以自定义:

js
const synth = new Tone.Synth({
  oscillator: { type: 'triangle' },
  envelope: {
    attack: 0.02,
    decay: 0.2,
    sustain: 0.3,
    release: 0.4,
  },
}).toDestination()

振荡器类型包含 sine(纯净、柔和)、triangle(柔)、sawtooth(嗡嗡声)和 square(空洞、复古)。

构建音效系统

对游戏来说,你需要多个合成器来对应不同的声音类型。这是一个好用的模式:

js
let isInitialized = false
let softSynth = null
let subSynth = null
let noiseSynth = null
let sfxGain = null

function initSFX() {
  if (isInitialized) return
  
  // 主音量控制
  sfxGain = new Tone.Gain(0.7).toDestination()
  
  // 柔和的合成器用于旋律性声音
  softSynth = new Tone.PolySynth(Tone.Synth, {
    maxPolyphony: 6,
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.2,
      sustain: 0.3,
      release: 0.4,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // 用于深沉冲击的低频
  subSynth = new Tone.Synth({
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.3,
      sustain: 0.1,
      release: 0.3,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // 用于质感的噪声
  noiseSynth = new Tone.NoiseSynth({
    noise: { type: 'pink' },
    envelope: {
      attack: 0.02,
      decay: 0.15,
      sustain: 0,
      release: 0.15,
    },
    volume: -20,
  }).connect(sfxGain)
  
  isInitialized = true
}

PolySynth 让你同时播多个音。普通 Synth 是单音的。

游戏音效示例

一些常见的游戏音效以及制作方式:

收集金币——上行音符让人觉得有奖励感:

js
function coinCollect() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('C4', '8n', now, 0.3)
  softSynth.triggerAttackRelease('E4', '8n', now + 0.1, 0.35)
  softSynth.triggerAttackRelease('G4', '4n', now + 0.2, 0.4)
}

跳跃——快速上行滑音:

js
function jump() {
  const synth = new Tone.Synth({
    oscillator: { type: 'square' },
    envelope: { attack: 0.01, decay: 0.1, sustain: 0, release: 0.1 },
  }).toDestination()
  
  const now = Tone.now()
  synth.frequency.setValueAtTime(150, now)
  synth.frequency.exponentialRampToValueAtTime(400, now + 0.1)
  synth.triggerAttackRelease('C4', '8n', now, 0.2)
}

受击/伤害——下行 + 噪声:

js
function hit() {
  const now = Tone.now()
  subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
  noiseSynth.triggerAttackRelease('4n', now, 0.25)
}

死亡——悲伤的下行音符:

js
function death() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('E4', '4n', now, 0.4)
  softSynth.triggerAttackRelease('C4', '4n', now + 0.2, 0.35)
  softSynth.triggerAttackRelease('A3', '4n', now + 0.4, 0.3)
}

添加效果

效果让声音更丰富。把它们串在合成器和输出之间:

js
const filter = new Tone.Filter({
  frequency: 3000,
  type: 'lowpass',
  rolloff: -12,
})

const reverb = new Tone.Reverb({
  decay: 1.2,
  wet: 0.25,
})

const gain = new Tone.Gain(0.7)

// 链条:synth -> filter -> reverb -> gain -> destination
filter.connect(reverb)
reverb.connect(gain)
gain.toDestination()

const synth = new Tone.Synth().connect(filter)

低通滤波器切掉刺耳的高频。混响增加空间感。Gain 控制音量。

限流

如果游戏快速触发很多声音,它们会重叠成一团糟。加上限流:

js
const MIN_INTERVAL_MS = 35
let lastNoteTime = 0

function canPlayNote() {
  const now = performance.now()
  if (now - lastNoteTime < MIN_INTERVAL_MS) {
    return false
  }
  lastNoteTime = now
  return true
}

function playSound() {
  if (!canPlayNote()) return
  synth.triggerAttackRelease('C4', '8n')
}

动态背景音乐

让音乐响应玩法,用基于拍子的系统:

js
const BPM = 140
const BEAT_MS = (60 / BPM) * 1000

let bassSynth = null
let kickDrum = null
let beatInterval = null
let beatCount = 0
let energy = 0.3

const PROGRESSION = [
  { root: 'E2', chord: ['E3', 'G3', 'B3'] },
  { root: 'C2', chord: ['C3', 'E3', 'G3'] },
  { root: 'D2', chord: ['D3', 'F#3', 'A3'] },
  { root: 'A1', chord: ['A2', 'C3', 'E3'] },
]

function playBeat() {
  const beatInMeasure = beatCount % 8
  const chordIndex = Math.floor(beatCount / 8) % PROGRESSION.length
  const chord = PROGRESSION[chordIndex]
  const now = Tone.now()
  
  // 第 1 拍的低音
  if (beatInMeasure === 0) {
    bassSynth.triggerAttackRelease(chord.root, '4n', now, 0.6)
  }
  
  // 底鼓——只在能量高时
  if (beatInMeasure === 0 && energy > 0.3) {
    kickDrum.triggerAttackRelease('C1', '8n', now, 0.4)
  }
  
  beatCount++
}

function startMusic() {
  beatInterval = setInterval(playBeat, BEAT_MS)
}

function stopMusic() {
  clearInterval(beatInterval)
}

// 游戏事件影响能量
function onEnemyKill() {
  energy = Math.min(1, energy + 0.1)
}

音乐强度会随着游戏发生的事而变化。

用 MembraneSynth 和 NoiseSynth 做鼓

打击乐用专用合成器:

js
// 底鼓
const kick = new Tone.MembraneSynth({
  pitchDecay: 0.05,
  octaves: 4,
  envelope: {
    attack: 0.001,
    decay: 0.2,
    sustain: 0,
    release: 0.1,
  },
  volume: -14,
}).toDestination()

kick.triggerAttackRelease('C1', '8n')

// 军鼓
const snare = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.002,
    decay: 0.15,
    sustain: 0,
    release: 0.1,
  },
  volume: -12,
}).toDestination()

snare.triggerAttackRelease('8n')

// 踩镲
const hihat = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.001,
    decay: 0.04,
    sustain: 0,
    release: 0.03,
  },
  volume: -22,
}).toDestination()

hihat.triggerAttackRelease('32n')

清理

不用时销毁合成器,释放资源:

js
function cleanup() {
  if (softSynth) softSynth.dispose()
  if (subSynth) subSynth.dispose()
  if (noiseSynth) noiseSynth.dispose()
  if (sfxGain) sfxGain.dispose()
}

常见错误

没等 Tone.start()——音频上下文必须在用户交互里启动,不然不会播。

每次发声都新建合成器——init 时创建一次反复使用。每次都新建会泄漏内存。

太多重叠声音——用限流和去重来防止音频糊成一团。

刺耳频率——用低通滤波器和更柔和的振荡器类型(sine、triangle),玩家就不会反感。

更多资源

游戏的 Web Audio API 涵盖底层 API 和音频文件加载。

Tone.js 文档 提供完整 API 参考。

去哪里找免费游戏素材 包含 Freesound 等音频与音效来源。

移动端友好的网页游戏 涵盖 iOS 音频限制和触摸解锁模式。