Web Audio API 允许我们在浏览器中直接生成声音。它使您的网站、应用程序和游戏更加有趣和引人入胜。您甚至可以构建特定于音乐的应用程序,例如鼓机和合成器。在本文中,我们将通过构建一些有趣且简单的项目来学习如何使用 Web Audio API。
入门
让我们先了解一些术语。Web Audio API 中的所有音频操作都在一个音频上下文中处理。每个基本的音频操作都由音频节点执行,这些节点连接在一起,形成一个音频路由图。在播放任何声音之前,您需要创建此音频上下文。这与我们如何使用<canvas>
元素创建绘图上下文非常相似。以下是我们如何创建音频上下文
var context = new (window.AudioContext || window.webkitAudioContext)();
Safari 需要 webkit 前缀来支持 AudioContext,因此您应该使用该行而不是new AudioContext();
通常,Web Audio API 的工作流程如下所示

有三种类型的源
- 振荡器 – 数学计算的声音
- 音频样本 – 来自音频/视频文件
- 音频流 – 来自网络摄像头或麦克风的音频
让我们从振荡器开始
振荡器是一种重复的波形。它具有频率和峰值振幅。除了频率和振幅之外,振荡器的最重要的特性之一是其波形的形状。四个最常用的振荡器波形是正弦波、三角波、方波和锯齿波。

也可以创建自定义形状。不同的形状适用于不同的合成技术,它们会产生不同的声音,从平滑到刺耳。
Web Audio API 使用OscillatorNode
来表示重复的波形。我们可以使用上面显示的所有波形形状。为此,我们必须像这样分配value属性
OscillatorNode.type = 'sine'|'square'|'triangle'|'sawtooth';
您也可以创建一个自定义波形。您使用setPeriodicWave()
方法创建波形的形状,这将自动将类型设置为custom。让我们听听不同的波形如何产生不同的声音
自定义波形是使用傅里叶变换创建的。如果您想了解有关自定义波形形状的更多信息(例如如何制作警笛),您可以从这里获得很好的资源。
运行振荡器
让我们尝试发出一些声音。为此,我们需要以下步骤
- 我们必须创建一个 Web Audio API 上下文
- 在该上下文中创建振荡器节点
- 选择波形类型
- 设置频率
- 将振荡器连接到目标
- 启动振荡器
让我们将这些步骤转换为代码。
var context = new (window.AudioContext || window.webkitAudioContext)();
var oscillator = context.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = 440;
oscillator.connect(context.destination);
oscillator.start();
请注意我们如何定义音频上下文。Safari 需要webkit
前缀,因此我们使其跨浏览器兼容。
然后我们创建振荡器并设置波形的类型。类型的默认值为sine
,因此您可以跳过此行,我只是喜欢添加它以使其更清晰且易于更新。我们将频率值设置为 440,这是 A4 音符(这也是默认值)。音乐音符 C0 到 B8 的频率范围为 16.35 到 7902.13Hz。我们将在本文后面查看一个播放许多不同音符的示例。
现在我们知道了所有这些,让我们也使音量可调。为此,我们需要在上下文中创建增益节点,将其连接到链中,并将增益连接到目标。
var gain = context.createGain();
oscillator.connect(gain);
gain.connect(context.destination);
var now = context.currentTime;
gain.gain.setValueAtTime(1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
oscillator.start(now);
oscillator.stop(now + 0.5);
现在您已经了解了如何使用振荡器,这是一个很好的练习。此示例 设置了振荡器代码。尝试创建一个简单的应用程序,当您上下移动屏幕光标时更改音量,当您左右移动光标时更改频率。
Web Audio API 的时间控制
构建音频软件时最重要的事情之一是管理时间。对于这里需要的精度,使用 JavaScript 时钟不是最佳实践,因为它根本不够精确。但是 Web Audio API 带有currentTime
属性,这是一个不断增加的双精度硬件时间戳,可用于安排音频播放。它在声明音频上下文时从 0 开始。尝试运行console.log(context.currentTime)
以查看时间戳。
例如,如果您希望振荡器立即播放,则应运行oscillator.start(0)
(您可以省略 0,因为它是默认值)。但是,您可能希望它从现在开始的一秒钟后开始播放,播放两秒钟,然后停止。以下是操作方法
var now = context.currentTime;
oscillator.play(now + 1);
oscillator.stop(now + 3);
这里有两个方法需要提及。
AudioParam.setValueAtTime(value, startTime)
方法在精确的时间安排值的更改。例如,您希望在一秒钟内更改振荡器的频率值
oscillator.frequency.setValueAtTime(261.6, context.currentTime + 1);
但是,当您想要立即更新值时,您也可以使用它,例如.setValueAtTime(value, context.currentTime)
。您可以通过修改AudioParam
的value属性来设置值,但是如果在自动化事件(使用AudioParam
方法安排的事件)发生的同时更新值,则任何对值的更新都会被忽略,而不会引发异常。
AudioParam.exponentialRampToValueAtTime(value, endTime)
方法安排值的逐渐变化。此代码将在 1 秒内以指数方式降低振荡器的音量,这是一种平滑停止声音的好方法
gain.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 1);
我们不能使用 0 作为值,因为值需要为正,所以我们改为使用一个非常小的值。
创建声音类
停止振荡器后,您无法再次启动它。您没有做错任何事,这是 Web Audio API 的一项优化性能的功能。我们可以做的是创建一个声音类,它将负责创建振荡器节点,以及播放和停止声音。这样,我们就可以多次调用声音。我将为此使用 ES6 语法
class Sound {
constructor(context) {
this.context = context;
}
init() {
this.oscillator = this.context.createOscillator();
this.gainNode = this.context.createGain();
this.oscillator.connect(this.gainNode);
this.gainNode.connect(this.context.destination);
this.oscillator.type = 'sine';
}
play(value, time) {
this.init();
this.oscillator.frequency.value = value;
this.gainNode.gain.setValueAtTime(1, this.context.currentTime);
this.oscillator.start(time);
this.stop(time);
}
stop(time) {
this.gainNode.gain.exponentialRampToValueAtTime(0.001, time + 1);
this.oscillator.stop(time + 1);
}
}
我们将上下文传递给构造函数,以便我们可以在同一个上下文中创建Sound
类的所有实例。然后我们有init
方法,它创建振荡器和所有必要的滤波器节点,连接它们等。Play
方法接受值(它将播放的音符的频率,以赫兹为单位)和它应播放的时间。但首先,它创建振荡器,并且每次我们调用play
方法时都会发生这种情况。stop
方法在一秒钟内以指数方式降低音量,直到完全停止振荡器。因此,每当我们需要再次播放声音时,我们都会创建一个新的sound
类实例并调用play方法。现在我们可以播放一些音符了
let context = new (window.AudioContext || window.webkitAudioContext)();
let note = new Sound(context);
let now = context.currentTime;
note.play(261.63, now);
note.play(293.66, now + 0.5);
note.play(329.63, now + 1);
note.play(349.23, now + 1.5);
note.play(392.00, now + 2);
note.play(440.00, now + 2.5);
note.play(493.88, now + 3);
note.play(523.25, now + 3.5);
这将在同一个上下文中播放 C D E F G A B C。如果您想知道音符的频率(以赫兹为单位),可以在这里找到它们。
了解了所有这些,我们就可以构建像木琴一样的东西!它创建一个新的Sound
实例并在mouseenter
时播放它。您可以查看示例并尝试自己做一个作为练习。
查看示例 播放木琴(Web Audio API),由 Greg Hovanesyan (@gregh) 在 CodePen 上发布。
我创建了一个游乐场,其中包含所有必需的 HTML 和 CSS,以及我们创建的Sound
类。使用data-frequency
属性获取音符值。在此尝试。
使用录制的声音
现在你已经用振荡器构建了一些东西,让我们看看如何处理录制的声音。有些声音很难用振荡器来再现。在很多情况下,为了使用真实的声音,你必须使用录制的声音。这可以是`.mp3`、`.ogg`、`.wav`等。查看完整列表以获取更多信息。我喜欢使用`.mp3`,因为它轻量级、支持广泛,并且具有相当好的音质。
你不能像处理图片那样简单地通过URL获取声音。我们必须运行一个XMLHttpRequest
来获取文件,解码数据,并将数据放入缓冲区。
class Buffer {
constructor(context, urls) {
this.context = context;
this.urls = urls;
this.buffer = [];
}
loadSound(url, index) {
let request = new XMLHttpRequest();
request.open('get', url, true);
request.responseType = 'arraybuffer';
let thisBuffer = this;
request.onload = function() {
thisBuffer.context.decodeAudioData(request.response, function(buffer) {
thisBuffer.buffer[index] = buffer;
updateProgress(thisBuffer.urls.length);
if(index == thisBuffer.urls.length-1) {
thisBuffer.loaded();
}
});
};
request.send();
};
loadAll() {
this.urls.forEach((url, index) => {
this.loadSound(url, index);
})
}
loaded() {
// what happens when all the files are loaded
}
getSoundByIndex(index) {
return this.buffer[index];
}
}
让我们看看构造函数。我们像在Sound
类中一样在那里接收我们的上下文,接收将加载的URL列表,以及缓冲区的空数组。
然后我们有两个方法:loadSound
和loadAll
。loadAll
循环遍历URL列表并调用loadSound
方法。传递索引非常重要,这样无论哪个请求先加载,我们都将缓冲的声音放入数组的正确元素中。这也让我们可以看到哪个请求是最后一个,这意味着在其完成后缓冲区已加载。
然后你可以调用loaded()
方法,它可以执行一些操作,例如隐藏加载指示器。最后,getSoundByIndex(index)
方法通过索引从缓冲区获取声音以进行播放。
decodeAudioData
方法有一个更新的基于Promise的语法,但它在Safari中尚不可用。
context.decodeAudioData(audioData).then(function(decodedData) {
// use the decoded data here
});
然后我们必须为声音创建类。现在我们有了用于处理录制声音的完整类。
class Sound() {
constructor(context, buffer) {
this.context = context;
this.buffer = buffer;
}
init() {
this.gainNode = this.context.createGain();
this.source = this.context.createBufferSource();
this.source.buffer = this.buffer;
this.source.connect(this.gainNode);
this.gainNode.connect(this.context.destination);
}
play() {
this.setup();
this.source.start(this.context.currentTime);
}
stop() {
this.gainNode.gain.exponentialRampToValueAtTime(0.001, this.context.currentTime + 0.5);
this.source.stop(this.context.currentTime + 0.5);
}
}
构造函数接受上下文和缓冲区。我们通过调用createBufferSource()
方法来创建,而不是像以前那样调用createOscillator
。缓冲区是我们使用getSoundByIndex()
方法获取的音符(缓冲区数组中的元素)。现在,我们不是创建振荡器,而是创建缓冲区源,设置缓冲区,然后将其连接到目标(或增益和其他滤波器)。
let buffer = new Buffer(context, sounds);
buffer.loadAll();
sound = new Sound(context, buffer.getSoundByIndex(id));
sound.play();
现在我们必须创建一个缓冲区实例并调用loadAll
方法,以将所有声音加载到缓冲区中。我们还有getSoundById
方法来获取我们需要的精确声音,因此我们将声音传递给Sound
并调用play()
。id
可以存储为单击以播放声音的按钮上的数据属性。
这是一个使用所有这些内容的项目:缓冲区、录制的音符等。
查看Greg Hovanesyan在CodePen上的笔The Bluesman – You Can Play The Blues (Web Audio API) (@gregh)。
你可以使用该示例作为参考,但为了你自己的练习,我创建了一个游乐场。它包含所有必要的HTML和CSS,以及我在真实在电吉他上录制的音符的URL。尝试编写你自己的代码!
滤波器简介
Web Audio API允许你在声源和目标之间添加不同的滤波器节点。BiquadFilterNode
是一个简单的低阶滤波器,它让你可以控制哪些频率部分应该被强调,哪些部分应该被衰减。这使你能够构建均衡器应用程序和其他效果。有8种类型的双二次滤波器:高通、低通、带通、低架、高架、峰值、陷波和全通。
高通滤波器可以很好地通过高频,但会衰减信号的低频分量。低通滤波器通过低频,但会衰减高频。它们也称为“低切”和“高切”滤波器,因为这解释了信号发生了什么。
高架和低架滤波器用于控制声音的低音和高音。它们用于强调或降低给定频率以上或以下的信号。
你会在BiquadFilterNode
接口中找到一个Q属性,它是一个双精度数,表示Q因子。品质因数或Q因子控制带宽,即受影响的频率数量。Q因子越低,带宽越宽,这意味着受影响的频率越多。Q因子越高,带宽越窄。
你可以在此处找到有关滤波器的更多信息,但我们已经可以构建参数均衡器了。它是一个均衡器,可以完全控制调整频率、带宽和增益。
让我们构建一个参数均衡器。
让我们看看如何将失真应用于声音。如果你想知道是什么让电吉他听起来像电吉他,那就是失真效果。我们使用WaveShaperNode接口来表示非线性失真器。我们需要做的是创建一个曲线来塑造信号,使其失真并产生特有的声音。我们不必花太多时间来创建曲线,因为它已经为我们做好了。我们也可以调整失真量。
结语
现在你已经了解了如何使用Web Audio API,我建议你自己尝试一下并创建自己的项目!
以下是一些用于处理Web音频的库
- Pizzicato.js – Pizzicato旨在简化你通过Web Audio API创建和操作声音的方式。
- webaudiox.js – webaudiox.js是一组辅助工具,将使使用Web Audio API变得更容易。
- howler.js – 现代网络的Javascript音频库。
- WAD – 使用HTML5 Web Audio API进行动态声音合成。它就像你耳朵的jQuery。
- Tone.js – 一个Web Audio框架,用于在浏览器中制作交互式音乐。
我刚刚花了太多时间用那把吉他即兴演奏了;)
我基本上浪费了整个午餐休息时间。
我花了几个小时弹吉他和小号!!!太好了!!
现在,我需要完成我的工作 :(
这种失真就像吉他的网络版效果,太棒了!
我和Gregor Adams一样。
精彩的教程。
易于理解,但涵盖了所有深入的细节
(是的,它里面有数学)。
我做了一个Web Audio合成器,它包含大多数Web Audio节点,以及一些艺术化的视觉反馈,我希望你喜欢它!:) https://venerons.github.io/Comet
Vernons,这太棒了,我玩你的Web Audio合成器玩得很开心!应该有一个录制选项!
非常酷,Venerons!
当你想要编写跨浏览器的Web音频代码时,还需要了解一些小事。我写了一篇关于这个主题的小博文:http://funktion.fm/news/web-audio-in-production
哦,天哪,这篇文章太好了。已添加到书签。干得好!
哇,这是一篇令人难以置信的文章。非常感谢分享一些见解。随时分享更多关于如何使用Web Audio API以及如何编程合成器/鼓机的知识:) 我一定会仔细阅读每一部分!干杯。
哇!这个教程非常酷。真的很喜欢这把吉他。谢谢!
很棒的文章。我做了一个简单的合成器并实现了一个步进音序器,但由于某种原因,如果我让它运行一段时间,音频会慢慢变小,但只有在滤波器启用并连接到增益节点时才会发生这种情况。有谁知道为什么吗?
我是JS新手,尝试了你的代码。我发现了一些让我的测试起作用的东西
不确定在哪里定义了:“updateProgress(thisBuffer.urls.length);”,所以我注释掉了。
在基于音频文件的第二个“class Sound () {…}”中,Firefox似乎不喜欢这个,所以不得不去掉()。
仍然在第二个“class sound”中,“this.setup()”应为“this.init();”。
谢谢
太棒的文章了,我花了1个多小时在这上面,玩了吉他以及代码……感谢分享 :)
感谢你精彩的文章,Greg!
我很好奇,是否可以使用WebAudio“重现”简单的音效?
例如,我想“重现”维基百科上的警笛声,音频链接为
https://upload.wikimedia.org/wikipedia/commons/f/f9/Motorsirene_-_Feuerwehralarm.ogg
我知道我需要使用XHR(数组缓冲区)下载音频,解码,播放,并在每一帧(
https://mdn.org.cn/ru/docs/DOM/window.requestAnimationFrame
)获取此声音的“快照”。假设我完成了这些步骤,但如何仅使用WebAudio振荡器来“重现”此声音呢?
我希望它像程序运行的一系列振荡器。
你有什么想法吗?如何分析这些“快照”,并在所有频率的“噪音”中只选择一个或五个可以与振荡器一起使用的频率?