(Web Audio API) 振荡器节点错误:不能多次调用start函数。

33

当我启动我的振荡器,停止它,然后再次启动它时,会出现以下错误:

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/


似乎是实现的限制。您可以尝试在每个start()上创建一个新的OscillatorNode - Joaquín O
请注意,您不需要启动/停止振荡器,只需将它们静音(在它们和目标之间使用增益节点),使它们对输出没有任何贡献,并根据需要取消静音(通过使用setTargetAtTime进行ADSR塑形)(当然,如果您需要处理多个振荡器,则需要使用增益、压缩和限制器节点进行一些额外的工作,以便您不会炸毁任何人的扬声器)。 - Mike 'Pomax' Kamermans
4个回答

43

更好的方法是只启动oscillatorNode一次,需要时连接/断开图表中的oscillatorNode,即:

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

这是在静音Thermin中实现的方法(Mozilla Web音频API文档)


1
您也可以简单地调用 osc.disconnect() 来断开所有连接,请参见 https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect - diachedelic
请注意,此解决方案基于假定振荡器是“音频发生器”,实际上它们不是,它们是“信号”发生器。播放音频并不需要频繁启动和停止振荡器,而是需要使用增益节点(“音量旋钮”)控制其信号是否可听,将其设置为0以听不到任何声音,或者将其设置为“某个非零值”以听到声音(正如在本文的 MDN 链接中所解释的 Web Audio ,这可能是自回答发布以来被更新了)。 - Mike 'Pomax' Kamermans
在这里回答的问题中,jacksonkr特别提到要避免使用gain。 - Alice Oualouest
但是出于错误的原因。他们提到避免增益,因为他们认为那会被视为“不良惯例”和低效,而事实上,这正是正确的做法,任何持相同看法的人都应该摒弃使用增益是错误的观念。 - Mike 'Pomax' Kamermans

10

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

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).

2
每个浏览器页面生命周期内只能创建6个audioContext对象。虽然在我的情况下是4个,但非常感谢您的帮助,解决了我遇到的问题。 - brad

7

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

因此,你真正想要做的是控制信号:如果你让它通过,并将其连接到音频输出,我们就会听到它,如果你阻止它,我们就听不到它。所以,即使你认为使用增益节点是“不良实践”,那也完全相反。我们绝对要使用增益节点:

信号→音量控制→音频输出

在这个链条中,我们可以让信号永远运行(因为它应该这样),而可以使用音量控制来控制播放。例如,假设我们想在点击按钮时播放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时出现的噼啪声和爆裂声。在那种情况下,信号几乎肯定不会在我们设置音量的确切时刻为零,所以扬声器必须跳到新位置,产生这些可爱的裂缝声。我们不想要那些。

这也意味着我们不会构建任何新元素或发生任何分配或垃圾回收开销:我们仅仅设置控制最终传输到音频终点的信号类型的值:

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

我们完成了。振荡器一直在运行,就像实际的声音电路一样,在此过程中几乎不使用资源,我们通过切换音量来控制是否可以听到它。

当然,我们可以通过将该链封装在具有自己的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>


这对我来说比断开连接要好得多,确实消除了噼啪声和爆裂声。看起来这是更好的答案,至少对于音频应用来说。 - oelna
这个绝对是最好的选择,声音更加流畅,逻辑更加清晰。 - undefined

5
据我所知,振荡器只能被播放一次,这与精度有关,但至今没有人对此做出清晰的解释。决定采用“仅播放一次”的模型的人可能会认为,在序列中插入沉默的唯一选择是使用零音量设置。毕竟,这确实是断开和重新创建方法的唯一替代方案。

原因是“如果你需要启动/停止,实际上你需要的是音量1.0 / 音量0.0”,你应该使用增益节点,因为你不关心低级振荡器,你关心的是对该振荡器信号进行某些操作。关键在于振荡器只是一个振荡器,直到你决定用它生成的信号作为声源时,它才成为声源。 - Mike 'Pomax' Kamermans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接