(Web Audio API) Oscillator node error: cannot call start more than once

(Web Audio API) Oscillator node error: cannot call start more than once

当我启动振荡器时,停止它,然后再启动它;我收到以下错误:

Uncaught InvalidStateError: Failed to execute 'start' on 'OscillatorNode': cannot call start more than once.

显然我可以使用 gain 来“停止”音频,但我觉得这是一种糟糕的做法。在能够再次启动振荡器的同时停止振荡器的更有效方法是什么?

代码 (jsfiddle)

var ctx = new AudioContext();
var osc = ctx.createOscillator();

osc.frequency.value = 8000;

osc.connect(ctx.destination);

function startOsc(bool) {
    if(bool === undefined) bool = true;
    
    if(bool === true) {
        osc.start(ctx.currentTime);
    } else {
        osc.stop(ctx.currentTime);
    }
}

$(document).ready(function() {
    $("#start").click(function() {
       startOsc(); 
    });
    $("#stop").click(function() {
       startOsc(false); 
    });
});

当前解(提问时):http://jsfiddle.net/xbqbzgt2/2/

最终解决方案:http://jsfiddle.net/xbqbzgt2/3/

到目前为止,我找到的最佳解决方案是在每次需要使用时重新创建 oscillator 的同时保持相同的 audioContext

http://jsfiddle.net/xbqbzgt2/3/

仅供参考您只能在每个浏览器页面生命周期(或至少根据我的硬件)创建 6 个 audioContext 对象:

Uncaught NotSupportedError: Failed to construct 'AudioContext': The number of hardware contexts provided (6) is greater than or equal to the maximum bound (6).

更好的方法是启动振荡器节点一次,并在需要时从图表中connect/disconnect启动振荡器节点,即:

var ctx = new AudioContext();
var osc = ctx.createOscillator();   
osc.frequency.value = 8000;    
osc.start();    
$(document).ready(function() {
    $("#start").click(function() {
         osc.connect(ctx.destination);
    });
    $("#stop").click(function() {
         osc.disconnect(ctx.destination);
    });
});

muting the thermin(mozilla 网络音频 api 文档)

据我所知,振荡器只能播放一次,原因与精度有关,而且还没有人很好地解释过。决定使用此 "play only once" 模型的人可能会认为使用零音量设置在序列中间插入静音是一种很好的做法。毕竟,它确实是断开连接并重新创建方法的唯一替代方法。

虽然目前接受的答案有效,但有更好的方法可以做到这一点,基于振荡器不是“声源”的理解,它们是信号源,并且是“获得声音”的最佳方式" 并不是仅在您需要声音时才启动(一个或多个)振荡器,而是让它们已经 运行 宁,并根据需要简单地允许它们的信号通过或阻止它们。

因此,你想要做的是 gate the signal:如果你让它通过,并且它连接到音频输出,我们会听到它,如果你阻止它,我们就不会听到。我们要的是:

Signal → Volume control → Audio output

在这个链中,我们可以让信号 运行 永远(正如它应该的那样),我们可以使用音量控制来控制播放。例如,假设我们希望在单击按钮时播放 440Hz 的蜂鸣声。我们首先设置我们的链,一次:

// the "audio output" in our chain:
const audioContext = new AudioContext();

// the "volume control" in our chain:
const gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
gainNode.gain.setValueAtTime(0, audioContext.currentTime);

// the "signal" in our chain:
const osc = audioContext.createOscillator();
osc.frequency.value = 440;
osc.connect(gainNode);
osc.start();

然后为了发出哔哔声,我们使用 setTargetAtTime 函数将音量设置为 1,这让我们可以“在某个特定时间”更改参数,间隔时间(通常很短)价值从“它是什么”顺利地变成了“我们想要它成为什么”。我们这样做是因为我们不想要我们只使用 setValueAtTime 时得到的那种 crackling/popping:信号几乎可以保证在我们设置音量的那一刻不为零,所以扬声器必须跳到新的位置,给那些可爱的裂缝。我们不想要那些。

这也意味着我们没有构建任何新元素或生成器,没有分配或垃圾收集开销:我们只是设置控制信号类型的值最终到达音频目的地:

const smoothingInterval = 0.02;
const beepLengthInSeconds = 0.5;

playButton.addEventListener(`click`, () => {
  const now = audioContext.currentTime;
  gainNode.gain.setTargetAtTime(1, now, smoothingInterval);
  gainNode.gain.setTargetAtTime(0, now + beepLengthInSeconds, smoothingInterval);
});

我们完成了。振荡器总是 运行ning,就像在实际的声音电路中一样,在这样做时使用 near-zero 资源,我们控制是否可以 听到 打开和关闭音量。

当然,我们可以通过将链封装在具有自己的 play() 功能的东西中,使这个 更有用

const audioContext = new AudioContext();
const now = () => audioContext.currentTime;
const smoothingInterval = 0.02;
const beepLengthInSeconds = 0.5;
const beeps = [220,440,880].map(Hz => createBeeper(Hz));

playButton.addEventListener(`click`, () => {
  const note = (beeps.length * Math.random()) | 0;
  beeps[note].play();
});

function createBeeper(Hz=220, duration=beepLengthInSeconds) {
  const gainNode = audioContext.createGain();
  gainNode.connect(audioContext.destination);
  gainNode.gain.setValueAtTime(0, now());

  const osc = audioContext.createOscillator();
  osc.frequency.value = Hz;
  osc.connect(gainNode);
  osc.start();

  return {
    play: (howLong=duration) => {
      console.log(`playing ${Hz}Hz for ${howLong}s`);
      trigger(gainNode.gain, howLong);
    }
  };
}

function trigger(parameter, howLong) {
  parameter.setTargetAtTime(1, now(), smoothingInterval);
  parameter.setTargetAtTime(0, now() + howLong, smoothingInterval);
}
<button id="playButton">play</button>