使用 ConstantSourceNode 控制多个参数

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

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

解决方案很简单,它涉及使用一种音频节点类型,乍一看似乎没什么用处:ConstantSourceNode

技术

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

下图显示了它是如何工作的;输入值N设置为ConstantSourceNode.offset属性的值。ConstantSourceNode可以具有任意数量的输出;在本例中,我们将其连接到三个节点:两个GainNode和一个StereoPannerNode。因此,N成为指定参数的值(gain对于GainNode以及StereoPannerNode的平移)。

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>
</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, false);
}

首先,我们访问窗口的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, false);

切换振荡器的开/关

因为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.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

  1. 将振荡器的type设置为"sine",以使用正弦波作为音频波形。
  2. 将振荡器的frequency设置为所需的值;在本例中,oscNode1设置为中央C音,而oscNode2oscNode3则通过演奏E和G音来补充和弦。

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

创建完所有三个振荡器后,通过依次调用每个振荡器的ConstantSourceNode.start()方法来启动它们。

停止振荡器

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

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

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

结果

另请参阅