34

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/

2
  • It looks like a limitation of implementation.. You can try to create a new OscillatorNode on each start()
    – Joaquín O
    Commented Aug 23, 2015 at 1:10
  • Note that you shouldn't need to start/stop oscillators, you just need to mute them (using a gain node between them and their destination) so they contribute nothing to the output, and unmute them (with ADSR shaping through the use of setTargetAtTime) as needed (and of course, some extra work using gain, compressor, and limiter nodes if you need to deal with multiple oscillators at the same time, so you don't blow up anyone's speakers) Commented Mar 25, 2022 at 16:42

4 Answers 4

44

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)

4
  • 1
    You can also simply call osc.disconnect() to disconnect all connections, see developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect Commented Apr 22, 2020 at 6:19
  • Note that this is a solution based on assuming oscillators are "audio generators", which they're not, they're signal generators. Playing audio does not require starting and stopping the oscillator so much as it requires controlling whether or not its signal can be heard using a gain node ("a volume knob") that you set to 0 to hear nothing, or "some non-zero value" to hear something (as explained in the post's MDN link for Web Audio, which was probably updated since this answer was posted) Commented Mar 11, 2022 at 20:51
  • In the question answered here, jacksonkr specifically mentions avoiding the use of gain. Commented Dec 1, 2022 at 21:57
  • But for the wrong reasons. They mention avoiding gain because they think that would be considered "poor practice" and inefficient, when in fact fact that's exactly the right thing to do, and anyone who thinks the same should be disabused of the notion that using gain is wrong. Commented Mar 15, 2023 at 17:14
10

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).
1
  • 2
    "You can only create 6 audioContext objects per browser page lifespan". In my case this was 4, but thank you, this was very helpful and solved an issue I was having.
    – brad
    Commented Dec 3, 2018 at 12:12
8

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 really is to 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. So even though you might think that using a gain node is "poor practice", that's literally the opposite of what it is. We absolutely want to use a gain node:

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>

2
  • This worked a lot better for me than disconnecting and did indeed get rid of the crackling and popping noises. Seems like the better answer, at least for audio applications.
    – oelna
    Commented Jul 20, 2023 at 9:05
  • This one is definitely the way to go, the sound is much smoother and the logic is clearer. Commented Sep 10, 2023 at 11:17
5

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.

1
  • The reason being "if you need start/stop, what you actually need is volume 1.0/volume 0.0" and you should use a gain node, because you don't care about the low level oscillator, you care about doing something with that oscillator's signal. The key being that an oscillator is just an oscillator, it's not a sound source until you decide that's what you want to do with the signal it generates. Commented Mar 11, 2022 at 20:12

Not the answer you're looking for? Browse other questions tagged or ask your own question.