简介
音频是多媒体体验如此引人入胜的重要原因。如果你曾试过在静音状态下看电影, 可能已经注意到了这一点。
游戏也不例外!我最美好的电子游戏回忆是音乐和音效。现在,在玩过我最喜欢的游戏近二十年后,在许多情况下,我仍然无法忘记近藤浩治的《塞尔达传说》乐曲和马特·乌尔曼的《暗黑破坏神》氛围配乐。音效也同样具有这种吸引力,例如《魔兽争霸》中一听便知的单位点击响应,以及任天堂经典游戏中的音效。
游戏音频带来了一些有趣的挑战。为了创作出令人信服的游戏音乐,设计师需要根据玩家可能遇到的不可预测的游戏状态进行调整。在实践中,游戏的部分内容可能会持续未知时长,声音可能会与环境互动并以复杂的方式混合,例如房间效果和相对声音定位。最后,可能会同时播放大量声音,所有这些声音都需要和谐地融合在一起,并且在渲染时不会造成性能损失。
网页上的游戏音频
对于简单的游戏,使用 <audio> 标记可能就足够了。不过,许多浏览器提供的实现效果不佳,会导致音频故障和高延迟。希望这只是一个暂时性问题,因为供应商正在努力改进各自的实现。如需了解 <audio> 标记的状态,请访问 areweplayingyet.org,其中提供了一个不错的测试套件。
不过,深入研究 <audio> 标记规范后,您会发现有很多事情根本无法通过它来完成,这并不奇怪,因为它是专为媒体播放而设计的。部分限制包括:
- 无法对声音信号应用过滤条件
- 无法访问原始 PCM 数据
- 对声源和听者的位置和方向毫无概念
- 没有精细的时间控制。
在本文的其余部分,我将深入探讨其中一些主题,并结合使用 Web Audio API 编写的游戏音频。如需简要了解此 API,请参阅入门教程。
背景音乐
游戏通常会循环播放背景音乐。
如果循环短且可预测,可能会让人感到非常烦躁。如果玩家卡在某个区域或关卡,而同一段音乐在后台不断播放,那么最好逐渐淡出该曲目,以免玩家进一步感到沮丧。另一种策略是混合使用各种强度,并根据游戏情境逐渐交叉淡入淡出。
例如,如果玩家身处一个有史诗级 Boss 战的区域,您可能会有几种不同的混音,其情感范围从氛围营造到预示再到激烈。音乐合成软件通常允许您通过选择要在导出中使用的曲目集,根据一首乐曲导出多个混音(长度相同)。这样,您就可以保持一定的内部一致性,并避免在从一个曲目淡入淡出到另一个曲目时出现不和谐的过渡。
然后,您可以使用 Web Audio API,通过 XHR 使用 BufferLoader 类等内容导入所有这些样本(Web Audio API 简介文章对此进行了深入介绍)。加载声音需要时间,因此游戏中使用的素材资源应在网页加载时、关卡开始时或在玩家玩游戏时逐步加载。
接下来,您需要为每个节点创建一个源,为每个源创建一个增益节点,然后连接图。
完成此操作后,您可以同时循环播放所有这些音频源,并且由于它们的长度相同,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 的增益,您可以将混音降低到可防止削波的水平。不过,在实践中,由于游戏中播放的声音可能取决于各种各样的因素,因此可能很难确定可防止所有状态下发生削波的主增益值。一般来说,您应该调整增益以预测最坏的情况,但这更像是一门艺术,而不是一门科学。
加点糖
压缩器通常用于音乐和游戏制作,以平滑信号并控制整体信号中的尖峰。在 Web Audio 世界中,此功能通过 DynamicsCompressorNode 提供,可以插入到音频图中,以提供更响亮、更丰富、更饱满的声音,并有助于剪辑。直接引用规范,此节点
使用动态压缩通常是一个好主意,尤其是在游戏设置中,如前所述,您无法确切知道将播放哪些声音以及何时播放。DinahMoe 实验室的 Plink 就是一个很好的例子,因为播放的声音完全取决于您和其他参与者。在大多数情况下,压缩器都很有用,但也有一些罕见的情况,例如,您正在处理经过精心母带处理的曲目,这些曲目已经过调音,听起来“恰到好处”。
实现这一点只需在音频图中添加一个 DynamicsCompressorNode,通常作为目标之前的最后一个节点。
// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);
如需详细了解动态压缩,请参阅这篇维基百科文章,其中包含丰富的信息。
总而言之,请仔细聆听是否有削波,并通过插入主增益节点来防止削波。然后使用动态压缩器节点 来收紧整个混音您的音频图可能如下所示:
总结
以上内容涵盖了我认为使用 Web Audio API 进行游戏音频开发时最重要的方面。借助这些技术,您可以直接在浏览器中打造真正引人入胜的音频体验。在结束之前,我还要给您一个浏览器专属提示:如果您的标签页进入后台,请务必使用页面可见性 API 暂停声音,否则可能会给用户带来不好的体验。
如需详细了解 Web Audio,请参阅入门文章;如果您有疑问,请查看网络音频常见问题解答,看看是否已有解答。 最后,如果您还有其他问题,请在 Stack Overflow 上使用 web-audio 标签提问。
在结束之前,我先向您介绍一些在当今的真实游戏中使用 Web Audio API 的绝佳示例:
- Field Runners,以及一些技术细节的说明。
- Angry Birds 最近改用了 Web Audio API。如需了解详情,请参阅此文章。
- Skid Racer,这款游戏充分利用了空间音频。