33

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/

Community
  • 1
  • 1
Jacksonkr
  • 31,583
  • 39
  • 180
  • 284
  • It looks like a limitation of implementation.. You can try to create a new `OscillatorNode` on each `start()` – Joaquín O Aug 23 '15 at 01: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) – Mike 'Pomax' Kamermans Mar 25 '22 at 16:42

4 Answers4

43

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)

Alice Oualouest
  • 836
  • 12
  • 20
  • 1
    You can also simply call `osc.disconnect()` to disconnect all connections, see https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect – diachedelic Apr 22 '20 at 06: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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API#modifying_sound), which was probably updated since this answer was posted) – Mike 'Pomax' Kamermans Mar 11 '22 at 20:51
  • In the question answered here, jacksonkr specifically mentions avoiding the use of gain. – Alice Oualouest Dec 01 '22 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. – Mike 'Pomax' Kamermans Mar 15 '23 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).
Community
  • 1
  • 1
Jacksonkr
  • 31,583
  • 39
  • 180
  • 284
  • 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 Dec 03 '18 at 12:12
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 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>
Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
  • 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 Jul 20 '23 at 09:05
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.

Billy
  • 61
  • 1
  • 2
  • 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. – Mike 'Pomax' Kamermans Mar 11 '22 at 20:12