使用 ConstantSourceNode 控制多个参数

本文演示了如何使用 ConstantSourceNode 将多个参数链接在一起,使它们共享相同的值,该值可以通过设置 ConstantSourceNode.offset 参数的值来更改。

有时您可能希望将多个音频参数链接在一起,使它们在以某种方式更改时共享相同的值。例如,您可能有一组振荡器,其中两个需要共享相同的可配置音量,或者您有一个应用于特定输入但并非所有输入的滤波器。您可以逐个更改每个受影响 AudioParam 的值。尽管如此,这样做有两个缺点:首先,这是额外的代码,正如您即将看到的,您不必编写这些代码;其次,该循环会占用您线程(可能是主线程)上的宝贵 CPU 时间,并且有一种方法可以将所有这些工作卸载到音频渲染线程,该线程经过优化,可以处理此类工作,并且可能比您的代码具有更高的优先级。

解决方案很简单,它涉及使用一种看似用处不大的音频节点类型:ConstantSourceNode

技术

使用 ConstantSourceNode 是一种轻松完成听起来可能很难的事情的方法。您需要创建一个 ConstantSourceNode 并将其连接到所有应链接其值以始终匹配的 AudioParam。由于 ConstantSourceNodeoffset 值直接发送到其所有输出,因此它充当该值的分配器,将其发送到每个连接的参数。

下图显示了此工作原理;输入值 N 被设置为 ConstantSourceNode.offset 属性的值。ConstantSourceNode 可以拥有任意数量的输出;在这种情况下,我们已将其连接到三个节点:两个 GainNode 和一个 StereoPannerNode。因此,N 成为指定参数的值(对于 GainNode,它是 gain,对于 StereoPannerNode,它是 pan)。

Diagram in SVG showing how ConstantSourceNode can be used to split an input parameter to share it with multiple nodes.

结果是,每次更改 N(输入 AudioParam 的值)时,两个 GainNode.gain 属性的值以及 StereoPannerNodepan 属性的值都设置为 N

示例

让我们来看看这项技术的实际应用。在这个简单的例子中,我们创建了三个 OscillatorNode 对象。其中两个具有可调增益,通过共享输入控件进行控制。另一个振荡器具有固定的音量。

HTML

此示例的 HTML 内容主要是一个复选框,它被塑造成一个实际的按钮,用于切换振荡器音调的开/关,以及一个类型为 range<input> 元素,用于控制三个振荡器中两个的音量。

html
<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>

<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 代码。

设置

让我们先从全局变量初始化开始。

js
// 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;它将在用户操作后初始化。

playButtonvolumeControl

对播放按钮和音量控件元素的引用。

oscNode1oscNode2oscNode3

用于生成和弦的三个 OscillatorNode

gainNode1gainNode2gainNode3

提供三个振荡器各自音量的三个 GainNode 实例。gainNode2gainNode3 将使用 ConstantSourceNode 链接在一起,以具有相同的、可调节的值。

constantNode

用于一起控制 gainNode2gainNode3 值的 ConstantSourceNode

现在让我们看看 setup() 函数,它在用户第一次切换播放按钮时调用;它负责设置音频图的所有初始化任务。

js
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);
}

首先,我们获取对窗口 AudioContext 的访问,将引用存储在 context 中。然后,我们获取对控件小部件的引用,将 playButton 设置为引用播放按钮,将 volumeControl 设置为引用用户将用于调整链接振荡器对增益的滑块控件。

接下来,创建 GainNode gainNode1 来处理非链接振荡器 (oscNode1) 的音量。我们将该增益设置为 0.5。我们还创建 gainNode2gainNode3,将它们的值设置为与 gainNode1 匹配,然后将音量滑块的值设置为相同的值,以便它与它控制的增益级别保持同步。

创建完所有增益节点后,我们创建 ConstantSourceNode constantNode。我们将其输出连接到 gainNode2gainNode3 上的 gain AudioParam,并通过调用其 start() 方法来运行常量节点;现在它正在将值 0.5 发送到两个增益节点的两个值,并且对 constantNode.offset 的任何更改都将自动设置 gainNode2gainNode3 的增益(按预期影响它们的音频输入)。

最后,我们将所有增益节点连接到 AudioContextdestination,以便任何发送到增益节点的音频都能到达输出,无论该输出是扬声器、耳机、录音流还是任何其他类型的目的地。

然后,我们为音量滑块的 input 事件分配一个处理程序(请参阅 控制链接的振荡器 查看非常简短的 changeVolume() 方法)。

在声明 setup() 函数后,我们向播放复选框的 change 事件添加一个处理程序(有关 togglePlay() 方法的更多信息,请参阅 切换振荡器的开/关),一切准备就绪。让我们看看它是如何运行的。

js
playButton.addEventListener("change", togglePlay);

切换振荡器的开/关

由于 OscillatorNode 不支持暂停状态的概念,因此我们必须通过终止振荡器并在用户再次单击播放复选框以重新打开它们时再次启动它们来模拟它。让我们看看代码。

js
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() 函数,即链接振荡器对增益的滑块控件的事件处理程序,如下所示:

js
function changeVolume(event) {
  constantNode.offset.value = volumeControl.value;
}

该简单函数控制两个节点的增益。我们所要做的就是设置 ConstantSourceNodeoffset 参数的值。该值成为节点的常量输出值,馈送到其所有输出 gainNode2gainNode3

虽然这是一个基础示例,但试想一下一个拥有 32 个振荡器的合成器,其中有多个链接参数在许多已连接的节点中发挥作用。减少调整所有参数的操作次数将对代码大小和性能都大有裨益。

启动振荡器

当用户在振荡器未播放时单击播放/暂停切换按钮时,会调用 startOscillators() 函数。

js
function startOscillators() {
  oscNode1 = new OscillatorNode(context, {
    type: "sine",
    frequency: 261.6255653005986, // middle C$
  });
  oscNode1.connect(gainNode1);

  oscNode2 = new OscillatorNode(context, {
    type: "sine",
    frequency: 329.6275569128699, // E
  });
  oscNode2.connect(gainNode2);

  oscNode3 = new OscillatorNode(context, {
    type: "sine",
    frequency: 391.99543598174927, // G
  });
  oscNode3.connect(gainNode3);

  oscNode1.start();
  oscNode2.start();
  oscNode3.start();
}

三个振荡器中的每一个都以相同的方式设置,通过调用 OscillatorNode() 构造函数并带有两个选项来创建 OscillatorNode

  1. 将振荡器的 type 设置为 "sine" 以使用正弦波作为音频波形。
  2. 将振荡器的 frequency 设置为所需值;在此示例中,oscNode1 设置为中央 C,而 oscNode2oscNode3 通过播放 E 和 G 音符来完成和弦。

然后,我们将新的振荡器连接到相应的增益节点。

一旦所有三个振荡器都被创建,它们就会通过依次调用每个振荡器的 ConstantSourceNode.start() 方法来启动。

停止振荡器

当用户切换播放状态以暂停音调时停止振荡器,这与停止每个节点一样简单。

js
function stopOscillators() {
  oscNode1.stop();
  oscNode2.stop();
  oscNode3.stop();
}

通过调用每个节点的 ConstantSourceNode.stop() 方法来停止每个节点。

结果

另见