本文将演示如何通过 ConstantSourceNode
(恒定源节点)将多个参数关联在一起,以便它们共享同一个值,并且这个值可以通过设置ConstantSourceNode.offset
的值来进行更改。
有时候,你可能希望将多个音频参数关联起来,以便共享同一个值,同时还能以某种方式进行更改。例如,你可能有一组振荡器,其中两个需要共享相同的可配置的音量,也可能对某些特定输入应用了一个滤波器,但不是所有输入都应用了该滤波器。你可以使用循环,逐个更改每个受影响的音频参数(AudioParam
)的值。不过,这样做有两个缺点:第一,正如你即将看到的,这是额外的代码,你本不必编写;其次,该循环会占用你所在线程(很可能是主线程)中宝贵的 CPU
时间。现在有一种方法可以将所有这些工作转移到音频渲染线程,音频渲染线程针对这类工作进行了优化,而且其运行的优先级级别可能比你的代码更合适。
解决方案很简单,它用到了一种乍一看似乎没那么有用的音频节点类型:ConstantSourceNode
。
技术
处理这些听起来可能颇具难度的事情,使用 ConstantSourceNode
是一种轻松易行的办法。你需要创建一个恒定源节点,并将其连接到所有其值应相互关联、始终保持一致的音频参数(AudioParam
)上。由于 ConstantSourceNode
的 offset
值会直接发送到它的所有输出端,因此它相当于该值的一个分配器,会将该值发送到每一个与之相连的参数上。
下面的示意图展示了这一工作原理:一个输入值 N
被设置为 ConstantSourceNode.offset
属性的值。ConstantSourceNode
可以根据需要拥有任意数量的输出端;在本例中,我们将它连接到了三个节点上:两个GainNode
(增益节点)和一个StereoPannerNode
(立体声声像定位节点)。于是,N
就变成了指定参数的值(对于增益节点来说是 gain
,对于立体声声像定位节点来说是 pan
)。
因此,每当你更改 N
(输入音频参数的值)时,两个 GainNode.gain
属性值和一个 StereoPannerNode.pan
属性值都会被设置为 N
。
示例
让我们来看一看这项技术的实际应用情况。在这个简单的示例中,我们创建了三个振荡器节点(OscillatorNode
)。其中两个振荡器具有可调节的增益,通过一个共享的输入控件来进行控制,另一个振荡器则具有固定的音量。
HTML
这个示例的 HTML
内容主要包含一个复选框(其外观呈现为实际的按钮样式),用于开启和关闭振荡器音调,以及一个类型为 range
的 <input>
标签,用于控制三个振荡器中两个振荡器的音量。
<div class="controls">
<input type="checkbox" id="playButton">
<label for="playButton">Activate: </label>
<label for="volumeControl">Volume: </label>
<input type="range" min="0.0" max="1.0" step="0.01"
value="0.8" name="volume" id="volumeControl">
</div>
</div>
<p>Toggle the checkbox above to start and stop the tones, and use the volume control to
change the volume of the notes E and G in the chord.</p>
JavaScript
接下来逐段看一下 JavaScript
代码。
准备
首先来看看全局变量的初始化。
// Useful UI elements
const playButton = document.querySelector("#playButton");
const volumeControl = document.querySelector("#volumeControl");
// The audio context and the node will be initialized after the first request
let context = null;
let oscNode1 = null;
let oscNode2 = null;
let oscNode3 = null;
let constantNode = null;
let gainNode1 = null;
let gainNode2 = null;
let gainNode3 = null;
变量说明如下:
context
:所有音频节点所在的音频上下文(AudioContext
),它将在用户执行某个操作之后进行初始化;playButton
、volumeControl
:对播放按钮和音量控制元素的引用;oscNode1
、oscNode2
、oscNode3
:用于生成和弦的三个振荡器节点;gainNode1
、gainNode2
、gainNode3
:三个增益节点实例,它们为三个振荡器提供音量级别。gainNode2
和gainNode3
将通过ConstantSourceNode
关联在一起,以拥有相同且可调节的值;constantNode
:用于共同控制gainNode2
和gainNode3
的值的ConstantSourceNode
。
接下来看一下 setup()
函数,当用户首次切换播放按钮时就会调用该函数,它负责处理所有初始化任务,以构建音频图。
function setup() {
context = new AudioContext();
gainNode1 = new GainNode(context, {
gain: 0.5,
});
gainNode2 = new GainNode(context, {
gain: gainNode1.gain.value,
});
gainNode3 = new GainNode(context, {
gain: gainNode1.gain.value,
});
volumeControl.value = gainNode1.gain.value;
constantNode = new ConstantSourceNode(context, {
offset: volumeControl.value,
});
constantNode.connect(gainNode2.gain);
constantNode.connect(gainNode3.gain);
constantNode.start();
gainNode1.connect(context.destination);
gainNode2.connect(context.destination);
gainNode3.connect(context.destination);
// All is set up. We can hook the volume control.
volumeControl.addEventListener("input", changeVolume, false);
}
首先,我们获取 window
的 AudioContext
,并将其引用存储在 context
变量中。然后,获取对控制部件的引用,将 playButton
设置为对播放按钮的引用,将 volumeControl
设置为对滑块控件的引用,用户将使用该滑块控件来调节这对关联振荡器的增益。
接下来,创建增益节点 gainNode1
,用于处理未关联的振荡器 oscNode1
的音量。我们将其增益设置为 0.5
。我们还创建了 gainNode2
和 gainNode3
,将它们的值设置为与 gainNode1
相同,然后将音量滑块的值也设置为相同的值,这样它就能与它所控制的增益级别保持同步。
一旦所有的增益节点都创建好之后,接下来就可以创建 ConstantSourceNode
了,即 constantNode
,将它的输出连接到 gainNode2
和 gainNode3
这两个节点上的 gain
音频参数,然后通过调用 start()
方法来启动这个恒定节点,现在它正将数值 0.5
发送给这两个增益节点的值,并且对 constantNode.offset
的任何更改都将自动设置 gainNode2
和 gainNode3
的增益(按预期影响它们的音频输入)。
最后,将所有增益节点都连接到 AudioContext.destination
,这样一来,任何传送到增益节点的声音都将抵达输出端,无论该输出端是扬声器、耳机、录音流,还是任何其他类型的输出目标。
接下来,我们为音量滑块的 input
事件指定一个处理程序(changeVolume()
方法详见“控制连接的振荡器”部分)。
在声明 setup()
函数之后,我们立即为播放复选框(实际上呈现为按钮样式)的 change
事件添加一个处理程序(togglePlay()
方法详见“开启和关闭振荡器”部分),至此准备工作就完成了。让我们看看实际操作情况是怎样的吧。
playButton.addEventListener("change", togglePlay, false);
开启和关闭振荡器
由于振荡器节点(OscillatorNode
)没有提供暂停状态的概念,所以我们必须通过终止振荡器并在用户再次点击播放复选框将其重新打开时重新启动来模拟它,代码如下:
function togglePlay(event) {
if (!playButton.checked) {
stopOscillators();
} else {
// If it is the first start, initialize the audio graph
if (!context) {
setup();
}
startOscillators();
}
}
如果 playButton
处于未被选中状态,那就意味着振荡器已经在播放了,此时我们会调用 stopOscillators()
函数来关闭这些振荡器。有关该代码的内容,请查看下面的 “停止振荡器” 部分。
如果 playButton
部件处于被选中状态,意味着当前处于暂停状态,那么我们就会调用 startOscillators()
函数来启动振荡器,使其开始发出音调。下面我们会在 “启动振荡器” 部分对该代码进行描述。
控制连接的振荡器
changeVolume()
函数,即关联振荡器上增益滑块控件的事件处理程序,如下所示:
function changeVolume(event) {
constantNode.offset.value = volumeControl.value;
}
这个简单的函数控制着两个节点的增益。我们所要做的就是设置 ConstantSourceNode
的 offset
参数的值。该值会成为这个节点的恒定输出值,并输送到它的所有输出端,也就是 gainNode2
和 gainNode3
。
虽然这只是一个简单的示例,但想象一下,假设有一个具备 32
个振荡器的合成器,在众多连接的节点间有多个相互关联的参数在起作用。减少调整这些参数的操作次数,对于代码规模和性能来说都将是极其重要的。
启动振荡器
当振荡器未播放时,用户点击播放/暂停切换按钮,就会调用 startOscillators()
函数。
function startOscillators() {
oscNode1 = new OscillatorNode(context, {
type: "sine",
frequency: 261.625565300598634, // middle C$
});
oscNode1.connect(gainNode1);
oscNode2 = new OscillatorNode(context, {
type: "sine",
frequency: 329.627556912869929, // E
});
oscNode2.connect(gainNode2);
oscNode3 = new OscillatorNode(context, {
type: "sine",
frequency: 391.995435981749294, // G
});
oscNode3.connect(gainNode3);
oscNode1.start();
oscNode2.start();
oscNode3.start();
}
这三个振荡器都是以相同方式设置的,即通过调用带有两个参数选项的 OscillatorNode()
构造函数来创建OscillatorNode
(振荡器节点):
- 将振荡器的
type
设置为sine
,以便使用正弦波作为音频波形; - 将振荡器的
frequency
设置为所需的值;在这种情况下,oscNode1
被设置为中央C
音,而oscNode2
和oscNode3
通过演奏E
音和G
音来完善这个和弦。
然后,我们将新的振荡器连接到相应的增益节点上。
一旦这三个振荡器全都创建完毕,就依次调用每个振荡器对应的 ConstantSourceNode.start()
方法来启动它们。
停止振荡器
当用户切换播放状态以暂停音调时,停止振荡器就如同逐个停止每个节点一样简单。
function stopOscillators() {
oscNode1.stop();
oscNode2.stop();
oscNode3.stop();
}
通过调用每个节点的ConstantSourceNode.stop()
方法来停止它们。
运行结果
在线运行详见 英文原文 的最后一小节。
评论0
暂时没有评论