2015 年 1 月,我的朋友兼合作者 Brian Belet 和我在第一届 Web Audio 大会 上展示了 Oiseaux de Même —— 一款基于鸟类录音创建的音频音景应用。在这篇文章中,我想描述一下我使用 Web Audio API、Twitter Bootstrap、Node.js 和 REST API 实现此应用的经验。
它是什么?具体音乐和公民科学
我们希望创建一个基于 Web 的 具体音乐,通过处理实地录音来构建艺术化的声音体验。我们决定使用 xeno-canto —— 一个包含超过 200,000 个 9,000 种不同鸟类的录音库 —— 作为我们的录音来源。几乎所有录音都由慷慨的录音师在知识共享许可下发布。我们根据来自 eBird 的数据从这个库中选择录音,eBird 是一个包含数千万条来自世界各地观鸟者贡献的鸟类目击信息的数据库。通过使用 地理位置 API 检索听众位置附近的 eBird 目击信息,我们的音景可以由观鸟者最近在听众附近报告的鸟类的录音组成 —— 每个用户都会获得一个每天都会变化的个性化音景。
Web Audio API 的使用
我们使用浏览器的 Web Audio API 播放来自 xeno-canto 的声音。Web Audio API 允许开发人员通过创建相互连接的 AudioNodes(就像旧的 模块化合成器)来播放、录制、分析和处理声音。
我们的音景使用四个 AudioBuffer 节点实现,每个节点循环播放一个实地录音。这些循环使用 Panner 节点放置在立体声场中,并在发送到听众的扬声器或耳机之前混合在一起。
控件
在所有声音加载并开始播放后,我们为用户提供了一些控件来在播放时操纵声音
- **Pan** 按钮随机化声音在 3D 空间中的空间位置。
- **Rate** 按钮随机化播放速度。
- **Reverse** 按钮反转声音播放方向。
- 最后,**Share** 按钮允许您捕获音景的状态并保存该快照以供以后使用。
上面描述的控件作为典型的 JavaScript 事件处理程序实现。例如,当按下**Pan** 按钮时,我们会运行此处理程序
// sets the X,Y,Z position of the Panner to random values between -1 and +1
BirdSongPlayer.prototype.randomizePanner = function() {
this.resetLastActionTime();
// NOTE: x = -1 is LEFT
this.panPosition = { x: 2 * Math.random() - 1, y: 2 * Math.random() - 1, z: 2 * Math.random() - 1}
this.panner.setPosition( this.panPosition.x, this.panPosition.y, this.panPosition.z);
}
Web Audio API 的某些部分是只写模式
我遇到了一些小问题,我不得不绕过 Web Audio API 的一些不足之处。其他作者已经记录了类似的经验;我将在本文中简要总结一下我的经验
- **无法读取 Panner 位置**: 在**Share** 按钮的事件处理程序中,我想检索并存储当前 Audio Buffer 播放速率和 Panner 位置。但是,当前的 Panner 节点在设置位置后不允许检索位置。因此,除了调用
setPosition()
之外,我还将新的 Panner 位置存储在实例变量中。到目前为止,这对我的代码的影响很小。我的长期担忧是,我宁愿将位置存储在 Panner 中并从那里检索它,而不是将副本存储在其他地方。以我的经验,随着代码变得越来越大越来越复杂,同一信息的多个副本会成为可读性和可维护性问题。
- **无法读取 AudioBuffer 的播放速率**: 上面描述的**Rate** 按钮在
playbackRate AudioParam
上调用linearRampToValueAtTime()
。据我所知,在调用linearRampToValueAtTime()
后,AudioParams 不允许我检索其值,因此我必须在我的 JS 对象中保留此值的副本。 - **无法读取 AudioBuffer 播放位置**: 我想向用户显示每个声音循环的当前播放位置,但是 API 没有提供此信息。我可以自己计算吗?不幸的是,在将 AudioBuffer 的
playbackRate
在随机值之间进行几次斜坡迭代后,很难计算缓冲区中的当前播放位置。与某些 API 用户不同,我不需要高度精确的位置,我只想在我的用户面前可视化当前声音循环何时重新开始。
使用 Web Audio 检查器进行调试
我非常成功地使用 Firefox 的 Web Audio 检查器来观察我的 Audio Nodes 在我的代码运行时如何创建和互连。
在上面的屏幕截图中,您可以看到四个 AudioBufferSource
,每个 AudioBufferSource
在被 AudioDestination
求和之前都通过 GainNode
和 PannerNode
传输。请注意,每个录音也连接到一个 AnalyzerNode
;分析器用于为每个循环创建滚动幅度图。
可视化声音循环
随着音景的发展,用户通常想知道哪种鸟类负责他们在混合中听到的特定声音。我们为每个循环使用一个滚动可视化来显示瞬时幅度,创建您可以与您听到的内容关联的独特形状。该可视化使用 Analyzer 节点对声音执行 快速傅里叶变换 (FFT),这会产生每个频率的声音幅度。我们计算所有这些幅度的平均值,然后在 Canvas 的右侧绘制该幅度。当 Canvas 的内容在每个动画帧上向侧面移动时,结果是一个水平滚动的幅度图。
BirdSongPlayer.prototype.initializeVUMeter = function() {
// set up VU meter
var myAnalyser = this.analyser;
var volumeMeterCanvas = $(this.playerSelector).find('canvas')[0];
var graphicsContext = volumeMeterCanvas.getContext('2d');
var previousVolume = 0;
requestAnimationFrame(function vuMeter() {
// get the average, bincount is fftsize / 2
var array = new Uint8Array(myAnalyser.frequencyBinCount);
myAnalyser.getByteFrequencyData(array);
var average = getAverageVolume(array);
average = Math.max(Math.min(average, 128), 0);
// draw the rightmost line in black right before shifting
graphicsContext.fillStyle = 'rgb(0,0,0)'
graphicsContext.fillRect(258, 128 - previousVolume, 2, previousVolume);
// shift the drawing over one pixel
graphicsContext.drawImage(volumeMeterCanvas, -1, 0);
// clear the rightmost column state
graphicsContext.fillStyle = 'rgb(245,245,245)'
graphicsContext.fillRect(259, 0, 1, 130);
// set the fill style for the last line (matches bootstrap button)
graphicsContext.fillStyle = '#5BC0DE'
graphicsContext.fillRect(258, 128 - average, 2, average);
requestAnimationFrame(vuMeter);
previousVolume = average;
});
}
接下来是什么
我正在继续清理此项目的 JavaScript 代码。我的 Mozilla 同事提出了一些用户界面改进建议,我想尝试一下。Belet 教授和我正在考虑我们可以使用哪些其他地理标记声音来源来制作更多音景。在此期间,请 亲自尝试 Oiseaux de Même 并告诉我们您的想法!
6 条评论