简介
音频是多媒体体验如此引人入胜的重要因素。如果您曾尝试过关闭音频观看电影,可能已经注意到了这一点。
游戏也不例外!我对视频游戏最美好的回忆就是音乐和音效。现在,在玩过我最喜欢的游戏近 20 年后,我仍然无法忘记 Koji Kondo 的《塞尔达传说》曲目和 Matt Uelmen 的氛围感十足的《暗黑破坏神》曲目。同样的吸引力也适用于音效,例如《魔兽世界》中一听就辨的单位点击响应,以及任天堂经典游戏中的选段。
游戏音频会带来一些有趣的挑战。为了创作出令人信服的游戏音乐,设计师需要适应玩家可能处于的不可预测的游戏状态。在实践中,游戏的某些部分可能会持续一段未知的时长,声音可以与环境互动,并以复杂的方式混合,例如房间效果和相对声音定位。最后,系统可以同时播放大量音效,所有这些音效都需要一起听起来不错,并且在渲染时不会降低性能。
网页上的游戏音频
对于简单的游戏,使用 <audio>
标记可能就足够了。不过,许多浏览器的实现方式不佳,导致音频出现故障和延迟时间较长。供应商正在努力改进各自的实现,因此这应该只是暂时性问题。如需大致了解 <audio>
代码的状态,请访问 areweplayingyet.org,其中提供了一个不错的测试套件。
不过,深入研究 <audio>
标记规范后,我们发现很多事情根本无法通过它完成,这并不奇怪,因为它是为媒体播放而设计的。一些限制包括:
- 无法对声音信号应用滤镜
- 无法访问原始 PCM 数据
- 不考虑声源和听众的位置和方向
- 没有精细的时间控制。
在本文的其余部分中,我将以使用 Web Audio API 编写的游戏音频为背景,深入探讨其中的一些主题。如需简要了解此 API,请参阅使用入门教程。
背景音乐
游戏通常会循环播放背景音乐。
如果循环短且可预测,可能会非常烦人。如果玩家卡在某个区域或关卡中,而背景中会持续播放同一选段,不妨考虑逐渐淡出该曲目,以免玩家感到更加沮丧。另一种策略是,根据游戏情境,让不同强度的混音逐渐淡化为彼此。
例如,如果玩家位于有史诗级老板战斗的区域,您可以准备多种混音,从氛围感、预示到激烈等情感范围各异。音乐合成软件通常允许您根据选定的曲目组合导出多首时长相同的混音曲目。这样,您就可以获得一些内部一致性,并避免在从一个轨道淡出到另一个轨道时出现突兀的转换。
然后,您可以使用 Web Audio API 通过 XHR 导入所有这些示例(Web Audio API 入门文章中对此进行了详细介绍),例如使用 BufferLoader 类。加载音效需要时间,因此游戏中使用的资源应在网页加载时、关卡开始时加载,或者在玩家玩游戏时逐步加载。
接下来,您需要为每个节点创建一个源,为每个源创建一个增益节点,并连接图表。
完成后,您可以同时循环播放所有这些来源,并且由于它们的长度都相同,Web Audio API 将保证它们保持同步。随着角色离最终的最终 Boss 战越来越近或越来越远,游戏可以使用增益量算法(如下所示)来调整链中各个节点的增益值:
// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
// If there is, adjust its gain.
gains[leftNode + 1].gain.value = gain2;
}
在上述方法中,两个音源会同时播放,并且我们会使用等功率曲线在它们之间进行交叉淡化(如前言中所述)。
缺失的环节:音频标记与 Web Audio
目前,许多游戏开发者都为其背景音乐使用 <audio>
标记,因为它非常适合在线播放内容。现在,您可以将 <audio>
标记中的内容引入 Web Audio 上下文。
由于 <audio>
标记可与在线内容搭配使用,因此此方法非常有用,您可以立即播放背景音乐,而无需等待其全部下载。通过将数据流引入 Web Audio API,您可以操控或分析数据流。以下示例会对通过 <audio>
标记播放的音乐应用低通滤波:
var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);
如需更详细地了解如何将 <audio>
标记与 Web Audio API 集成,请参阅这篇短文。
音效
游戏通常会播放音效来响应用户输入或游戏状态的变化。不过,与背景音乐一样,音效很快就会让人感到厌烦。为避免这种情况,通常最好准备一组类似但不同的音效来播放。这可能从轻微的脚步声音样本变化到剧烈的变化,如在《魔兽世界》系列游戏中,点击单位时所产生的变化。
游戏音效的另一个关键特性是,可以同时播放多个音效。假设您正在一场枪战中,有多名演员在射击机关枪。每挺机枪每秒会发射多次,导致同时播放数十个音效。同时从多个精确同步的来源播放声音是 Web Audio API 的一大亮点。
以下示例通过创建多个播放时间错开的声源,从多个单独的子弹声采样中创建了机枪弹药。
var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
var source = this.makeSource(this.buffers[M4A1]);
source.noteOn(time + i - interval);
}
现在,如果游戏中的所有机枪都完全这样响,那就太无聊了。当然,它们会因声音而异,具体取决于与目标的距离和相对位置(稍后会详细介绍),但即使这样可能还不够。幸运的是,Web Audio API 提供了两种方法来轻松调整上述示例:
- 子弹发射时间之间存在细微的变化
- 通过更改每个样本的 playbackRate(同时更改音调),以更好地模拟真实世界的随机性。
如需查看这些技术在实际应用中的更贴近真实的示例,请查看桌球演示,其中使用随机采样并调整 playbackRate 以获得更有趣的撞球声音。
3D 定位音效
游戏通常设定在具有某些几何图形属性的世界中,这些图形属性可能是 2D 或 3D 的。如果是这种情况,立体声定位音频可以显著提升沉浸式体验。幸运的是,Web Audio API 内置了硬件加速的定位音频功能,这些功能非常简单易用。顺便提一下,您应该确保自己有立体声扬声器(最好是耳机),这样才能听懂以下示例。
在上面的示例中,画布中间有一个监听器(人形图标),鼠标会影响来源(扬声器图标)的位置。以上是使用 AudioPannerNode 实现此类效果的简单示例。上述示例的基本思想是通过设置音频源的位置来响应鼠标移动,如下所示:
PositionSample.prototype.changePosition = function(position) {
// Position coordinates are in normalized canvas coordinates
// with -0.5 < x, y < 0.5
if (position) {
if (!this.isPlaying) {
this.play();
}
var mul = 2;
var x = position.x / this.size.width;
var y = -position.y / this.size.height;
this.panner.setPosition(x - mul, y - mul, -0.5);
} else {
this.stop();
}
};
有关 Web Audio 处理空间化方面的须知事项:
- 监听器默认位于原点 (0, 0, 0)。
- Web Audio 位置 API 没有单位,因此我引入了乘数,以使演示更好听。
- Web Audio 使用 y 轴向上的笛卡尔坐标(与大多数计算机图形系统相反)。因此,我在上面的代码段中交换了 y 轴
高级:声音锥
位置模型非常强大且非常先进,主要基于 OpenAL。如需了解详情,请参阅上面链接的规范的第 3 节和第 4 节。
有一个 AudioListener 附加到 Web Audio API 上下文,可通过位置和方向在空间中进行配置。每个源都可以通过 AudioPannerNode 传递,该节点会对输入音频进行空间化处理。平移节点具有位置和方向,以及距离和方向模型。
距离模型会根据与声源的距离指定增益量,而定向模型可以通过指定内锥和外锥来配置,这些锥决定了监听器位于内锥内、内锥和外锥之间或外锥外的增益量(通常为负值)。
var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;
虽然我的示例是二维的,但此模型很容易推广到第三个维度。如需查看 3D 空间化声音的示例,请参阅此位置信息示例。除了位置之外,Web Audio 声音模型还可以选择性地包含用于多普勒效应的速度。以下示例更详细地展示了多普勒效应。
如需详细了解此主题,请参阅有关 [混合位置音频和 WebGL][webgl] 的详细教程。
房间特效和滤镜
实际上,声音的听觉方式在很大程度上取决于听到声音的房间。与宽敞的开放式大厅相比,同一扇吱吱作响的门在地下室的声音会完全不同。制作水准精良的游戏会想要模仿这些效果,因为为每个环境创建一组单独的示例成本过高,并且会导致更多资源和更多游戏数据。
粗略地说,用于描述原始声音与实际声音之间差异的音频术语是脉冲响应。这些脉冲响应可以精心录制,事实上,有许多网站托管了这些预录制的脉冲响应文件(存储为音频),以便您使用。
如需详细了解如何根据给定环境创建脉冲响应,请参阅 Web Audio API 规范的卷积部分中的“录制设置”部分。
对我们而言,更重要的是,Web Audio API 提供了一种使用 ConvolverNode 轻松将这些脉冲响应应用于声音的方法。
// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);
另请参阅 Web Audio API 规范页面上的房间效果演示,以及此示例,您可以通过该示例控制一首优秀的爵士标准曲目的干音(原始)和湿音(通过混响器处理)混音。
倒计时已接近尾声
现在,您已经构建了一个游戏并配置了位置音频,图表中包含大量同时播放的 AudioNode。太棒了,但还有一件事需要考虑:
由于多个声音只是堆叠在一起,没有经过标准化处理,因此您可能会发现自己超出了音箱的性能阈值。与图片超出画布边界一样,如果波形超出其最大阈值,声音也会被剪裁,从而产生明显的失真。波形如下所示:
下面是一个实际运作的剪辑示例。波形看起来很糟糕:
请务必聆听上述那种刺耳的失真音效,或者相反,聆听过于柔和的混音,以免听众不得不调高音量。如果您遇到这种情况,就真的需要解决它!
检测剪裁
从技术层面来看,当任一声道中的信号值超出有效范围(即 -1 到 1 之间)时,就会发生剪裁。检测到这种情况后,最好提供视觉反馈来表明这种情况。为了可靠地执行此操作,请将 JavaScriptAudioNode 添加到图表中。音频图表的设置如下所示:
// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);
您可以在以下 processAudio
处理脚本中检测到剪裁:
function processAudio(e) {
var buffer = e.inputBuffer.getChannelData(0);
var isClipping = false;
// Iterate through buffer to check if any of the |values| exceeds 1.
for (var i = 0; i < buffer.length; i++) {
var absValue = Math.abs(buffer[i]);
if (absValue >= 1) {
isClipping = true;
break;
}
}
}
一般来说,出于性能方面的原因,请注意不要过度使用 JavaScriptAudioNode
。在这种情况下,衡量功能的替代实现可以在渲染时轮询音频图中的 RealtimeAnalyserNode
以获取 getByteFrequencyData
,具体取决于 requestAnimationFrame
。这种方法更高效,但会错过大部分信号(包括可能出现剪辑的地方),因为渲染最多每秒发生 60 次,而音频信号的变化速度要快得多。
由于剪辑检测非常重要,因此我们未来可能会看到内置的 MeterNode
Web Audio API 节点。
防止剪裁
通过调整主 AudioGainNode 的增益,您可以将混音调低到可防止剪裁的水平。不过,在实践中,由于游戏中播放的声音可能取决于各种各样的因素,因此很难确定一个能够防止所有状态出现 clipping 的主增益值。一般来说,您应调整增益以预测最糟糕的情况,但这更像是一门艺术,而不是一门科学。
加点糖
压缩器通常用于音乐和游戏制作,用于平滑信号并控制整体信号中的峰值。在 Web Audio 世界中,您可以通过 DynamicsCompressorNode
实现此功能。DynamicsCompressorNode
可插入音频图表中,以提供更响亮、更丰富、更饱满的声音,同时还能帮助防止音频剪辑。直接引用规范,此节点
通常,使用动态压缩功能是一个好主意,尤其是在游戏环境中,正如前面所述,您无法确切知道会播放什么声音以及何时播放。DinahMoe Labs 的 Plink 就是一个很好的例子,因为播放的声音完全取决于您和其他参与者。在大多数情况下,压缩器都很有用,但在某些罕见情况下,您需要处理经过精心母带制作且已调音至“恰到好处”的曲目。
实现此功能只需在音频图中添加 DynamicCompressorNode,通常将其作为目的地之前的最后一个节点即可:
// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);
如需详细了解动态压缩,请参阅这篇 Wikipedia 文章。
总而言之,请仔细聆听是否有削波,并通过插入主增益节点来防止削波。然后,使用动态压缩器节点收紧整个混音。您的音频图表可能如下所示:
总结
以上内容涵盖了我认为使用 Web Audio API 进行游戏音频开发时最重要的方面。借助这些技术,您可以在浏览器中打造真正引人入胜的音频体验。在结束本课之前,我要向您提供一个特定于浏览器的提示:如果您的标签页使用 page visibility API 进入后台,请务必暂停声音,否则可能会给用户带来令人沮丧的体验。
如需详细了解 Web Audio,请参阅更具入门性质的入门文章。如果您有疑问,请查看Web Audio 常见问题解答,看看问题是否已在其中得到解答。最后,如果您还有其他问题,请使用 web-audio 标记在 Stack Overflow 上提问。
在结束本课之前,我来介绍一下 Web Audio API 在现今真实游戏中的一些出色用法:
- Field Runner,以及有关一些技术细节的文章。
- Angry Birds,最近改用 Web Audio API。如需了解详情,请参阅这篇文章。
- Skid Racer,该游戏充分利用了空间音频。