案例研究:The Sounds of Racer
介绍
Racer 是一个多人、多设备的 Chrome 实验。 跨屏幕玩复古风格的老虎机游戏。 在手机或平板电脑、Android 或 iOS 上。 任何人都可以加入。 没有应用程序。 没有下载。 只是移动网络。
Plan8 与我们在 14islands 的朋友一起根据 Giorgio Moroder 的原创作品创造了充满活力的音乐和声音体验。 Racer 具有响应式引擎声音、比赛音效,但更重要的是动态音乐组合,当赛车手加入时,它会在多个设备上分布。 这是一个由智能手机组成的多扬声器装置。
将多个设备连接在一起是我们一直在玩弄的东西。 我们做过音乐实验,声音会在不同设备上分开或在设备之间跳跃,所以我们渴望将这些想法应用到 Racer。
更具体地说,我们想测试随着越来越多的人加入游戏,我们是否可以跨设备构建音乐轨道——从鼓和贝斯开始,然后添加吉他和合成器等等。 我们做了一些音乐演示并潜入编码。 多扬声器效果真的很棒。 在这一点上,我们还没有完全同步,但是当我们听到声音层层散布在设备上时,我们知道我们做得很好。
创造声音
谷歌创意实验室概述了声音和音乐的创意方向。 我们想使用模拟合成器来创建声音效果,而不是录制真实的声音或求助于声音库。 我们还知道,在大多数情况下,输出扬声器是小型手机或平板电脑扬声器,因此必须限制声音的频谱范围,以避免扬声器失真。 这被证明是一个相当大的挑战。 当我们收到 Giorgio 的第一批音乐草稿时,我们松了一口气,因为他的作品与我们创造的声音完美融合。
发动机声音
对声音进行编程的最大挑战是找到最佳的引擎声音并塑造其行为。 赛道类似于 F1 或纳斯卡赛道,因此赛车必须感觉快速且具有爆发力。 同时,这些汽车真的很小,所以大的发动机声音并不能真正将声音与视觉效果联系起来。 无论如何,我们无法在移动扬声器中播放强劲的咆哮引擎,因此我们必须找出其他东西。
为了获得一些灵感,我们连接了我们的一些朋友 Jon Ekstrand 的 模块化合成器集合并开始搞乱。 我们喜欢我们听到的。 这就是两个振荡器、一些不错的滤波器和 LFO 的声音。
之前使用 Web Audio API 对模拟设备进行了非常成功的改造,因此我们寄予厚望并开始在 Web Audio 中创建一个简单的合成器。 生成的声音将是最敏感的,但会占用设备的处理能力。 为了让视觉效果流畅运行,我们需要非常精简以节省所有资源。 所以我们改变了技术,转而播放音频样本。
有几种技术可用于从样本中产生引擎声音。 主机游戏最常见的方法是在不同的 RPM(有负载)下添加一层引擎的多种声音(越多越好),然后在它们之间进行交叉淡入淡出和交叉调音。 然后添加一层引擎的多个声音,以相同的 RPM 加速(无负载)以及交叉淡入淡出和交叉音高。 换档时这些层之间的交叉淡入淡出,如果处理得当,听起来会非常逼真,但前提是你有大量的声音文件。 交叉音高不能太宽,否则听起来会很合成。 由于我们必须避免长时间的加载,这个选项对我们不利。 我们尝试为每一层使用五六个声音文件,但声音令人失望。 我们必须找到一种文件更少的方法。
最有效的解决方案被证明是这样的:
- 一个带有加速和换档的声音文件,与汽车的视觉加速度同步,以最高音高/RPM 的编程循环结束。 Web Audio API 非常擅长精确循环,因此我们可以做到这一点而不会出现故障或爆音。
- 一个带有减速/发动机转速的声音文件。
- 最后一个声音文件循环播放静止/空闲声音。
看起来像这样
对于第一个触摸事件/加速,我们将从头开始播放第一个文件,如果玩家松开油门,我们将计算释放时声音文件中的时间,这样当油门再次打开时,它会跳转播放第二个(减速)文件后,在加速文件中的正确位置。
function throttleOn(throttle) { //Calculate the start position depending //on the current amount of throttle. //By multiplying throttle we get a start position //between 0 and 3 seconds. var startPosition = throttle * 3; var audio = context.createBufferSource(); audio.buffer = loadedBuffers["accelerate_and_loop"]; //Sets the loop positions for the buffer source. audio.loopStart = 5; audio.loopEnd = 9; //Starts the buffer source at the current time //with the calculated offset. audio.start(context.currentTime, startPosition); }
因此,只有三个小的声音文件和一个好的发声引擎,我们决定继续下一个挑战。
获取同步
我们与 14islands 的 David Lindkvist 一起开始深入研究如何让设备完美同步播放。 基本理论很简单。 设备向服务器询问其时间、网络延迟因素,然后计算本地时钟偏移。
syncOffset = localTime - serverTime - networkLatency
有了这个偏移量,每个连接的设备共享相同的时间概念。 容易,对吧? (再次,理论上。)
计算网络延迟
我们可以假设延迟是从服务器请求和接收响应所需时间的一半:
networkLatency = (receivedTime - sentTime) × 0.5
这个假设的问题是到服务器的往返行程并不总是对称的,即请求可能比响应花费更长的时间,反之亦然。 网络延迟越高,这种不对称性产生的影响就越大——导致声音延迟并与其他设备不同步。
幸运的是,如果声音稍微延迟,我们的大脑就不会注意到。 研究表明,我们的大脑需要延迟 20 到 30 毫秒 (ms) 才能将声音感知为独立的。 然而,在大约 12 到 15 毫秒时,您将开始“感觉到”延迟信号的影响,即使您无法完全“感知”它。 我们研究了几个已建立的时间同步协议、更简单的替代方案,并尝试在实践中实现其中的一些。 最后——多亏了谷歌的低延迟基础设施——我们能够简单地对一连串请求进行采样,并使用具有最低延迟的样本作为参考。
战斗时钟漂移
有效! 我们有 5 台以上的设备以完美同步的方式播放脉冲——但只是暂时的。 播放几分钟后,即使我们使用高度精确的 Web Audio API 上下文时间安排声音,设备也会分开。 延迟缓慢累积,一次只有几毫秒,起初无法察觉,但在播放较长时间后会导致音乐层完全不同步。 你好,时钟漂移。
解决方案是每隔几秒重新同步一次,计算一个新的时钟偏移并将其无缝输入音频调度程序。 为了降低由于网络延迟而导致音乐发生显着变化的风险,我们决定通过保留最新同步偏移的历史记录并计算平均值来平滑变化。
安排歌曲和切换安排
制作交互式声音体验意味着您不再控制歌曲的某些部分何时播放,因为您依赖用户操作来更改当前状态。 我们必须确保我们可以及时在歌曲中的安排之间切换,这意味着我们的调度程序必须能够在切换到下一个安排之前计算当前播放小节的剩余量。
我们的算法最终看起来像这样:
Client(1)
开始歌曲。Client(n)
歌曲开始时询问第一个客户。Client(n)
使用其 Web 音频上下文计算歌曲何时开始的参考点,并考虑 syncOffset 以及自创建音频上下文以来所经过的时间。playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
Client(n)
使用 playDelta 计算歌曲运行了多长时间。 歌曲调度程序使用它来知道接下来应该播放当前安排中的哪个小节。playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars
为了理智起见,我们将安排限制为始终为八小节长并且具有相同的节奏(每分钟节拍数)。
展望
总是很重要的 安排 使用时提前 setTimeout
或者 setInterval
在 JavaScript 中。 这是因为 JavaScript 时钟不是很精确,并且计划的回调很容易被布局、渲染、垃圾收集和 XMLHTTPRequests 扭曲数十毫秒或更长时间。 在我们的案例中,我们还必须考虑所有客户端通过网络接收相同事件所需的时间。
音频精灵
将声音合并到一个文件中是减少 HTTP 请求的好方法,对于 HTML 音频和 Web 音频 API 都是如此。 它也恰好是使用 Audio 对象响应式播放声音的最佳方式,因为它不必在播放前加载新的音频对象。 已经有一些 很好的实现 ,我们将其用作起点。 我们已经扩展了我们的 sprite 以在 iOS 和 Android 上可靠地工作,并处理一些设备休眠的奇怪情况。
在 Android 上,即使您将设备置于睡眠模式,音频元素也会继续播放。 在睡眠模式下,JavaScript 执行仅限于省电,您不能依赖 requestAnimationFrame
, setInterval
或者 setTimeout
触发回调。 这是一个问题,因为音频精灵依赖 JavaScript 来不断检查是否应该停止播放。 更糟糕的是,在某些情况下,音频元素的 currentTime
尽管仍在播放音频,但不会更新。
查看 AudioSprite 实现。 我们在 Chrome Racer 中用作非 Web 音频后备
音频元素
当我们开始开发 Racer 时,Android 版 Chrome 还不支持 Web Audio API。 某些设备使用 HTML 音频的逻辑,其他设备使用 Web 音频 API,结合我们想要实现的高级音频输出,带来了一些有趣的挑战。 谢天谢地,这已经成为历史。 Web Audio API 在 Android M28 beta 中实现。
- 延迟/时间问题。 当您告诉它播放时,音频元素并不总是准确地播放。 由于 JavaScript 是单线程的,浏览器可能很忙,导致播放延迟长达两秒。
- 播放延迟意味着平滑循环并不总是可能的。 在桌面上,您可以使用 双缓冲 来实现一些无间隙的循环,但在移动设备上,这不是一个选项,因为:
- 大多数移动设备一次不会播放多个音频元素。
- 固定音量。 Android 和 iOS 都不允许您更改 Audio 对象的音量。
- 没有预加载。 在移动设备上,Audio 元素不会开始加载其源,除非在
touchStart
处理程序。 - 求 问题 。 得到
duration
或设置currentTime
除非您的服务器支持 HTTP 字节范围,否则将失败。 如果您像我们一样构建音频精灵,请注意这一点。 - MP3 上的基本身份验证失败。 某些设备 都无法加载受 Basic Auth 保护的 MP3 文件。 无论您使用哪种浏览器,
结论
自从按下静音按钮作为处理网络声音的最佳选择以来,我们已经走了很长一段路,但这只是一个开始,网络音频即将大放异彩。 在同步多个设备方面,我们只触及了表面。 我们没有手机和平板电脑的处理能力来深入研究信号处理和效果(如混响),但随着设备性能的提高,基于网络的游戏也将利用这些功能。 这是继续推动声音可能性的激动人心的时刻。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 案例研究:Building Racer
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论