简体   繁体   中英

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

When I start my oscillator, stop it, and then start it again; I get the following error:

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

Obviously I could use gain to "stop" the audio but that strikes me as poor practice. What's a more efficient way of stopping the oscillator while being able to start it again?

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

Current solution (at time of question): http://jsfiddle.net/xbqbzgt2/2/

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

A better way would be to start the oscillatorNode once and connect/disconnect the oscillatorNode from the graph when needed, ie :

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

This how muting in done in muting the thermin (mozilla web audio api documentation)

The best solution I've found so far is to keep the SAME audioContext while recreating the oscillator every time you need to use it.

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

FYI You can only create 6 audioContext objects per browser page lifespan (or at least per my hardware):

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

While the currently accepted answer works, there is a much better way to do this, based on the understanding that oscillators are not "sound sources", they're signal sources, and the best way to "get a sound" is not to start up (one or more) oscillators only once you need the sound, but to have them already running, and simply allowing their signals through, or blocking them, as needed.

As such, what you want to do is gate the signal : if you let it through, and its connected to an audio out, we'll hear it, and if you block it, we won't hear it. What we want is this:

Signal → Volume control → Audio output

In this chain, we can let the signal run forever (as it's supposed to), and we can instead control playback using the volume control. For example, say we want to play a 440Hz beep whenever we click a button. We start by setting up our chain, once:

// 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();

And then in order to play a beep, we set the volume to 1 using the setTargetAtTime function, which lets us change parameters "at some specific time", with a (usually short) interval over which the value gets smoothly changed from "what it is" to "what we want it to be". Which we do because we don't want the kind of crackling/popping that we get when we just use setValueAtTime : the signal is almost guaranteed to not be zero the exact moment we set the volume, so the speaker has to jump to the new position, giving those lovely cracks. We don't want those.

This also means that we're not building any new elements, or generators, there's no allocation or garbage collection overhead: we just set the values that control what kind of signal ultimately makes it to the audio destination:

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

And we're done. The oscillator's always running, much like in actual sound circuitry, using near-zero resources while it does so, and we control whether or not we can hear it by toggling the volume on and off.

And of course we can make this much more useful by encapsulating the chain in something that has its own play() function:

 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>

From what I know, an oscillator can only be played once, for reasons having to do with precision, and never well explained by anyone yet. Whoever decided on this "play only once" model probably would consider it good practice to use a zero-volume setting to insert silence in the middle of a sequence. After all, it really is the only alternative to the disconnect-and-recreate method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM