Search code examples
javascriptsafaries6-promiseweb-audio-api

Safari — Cannot create Oscillator after the first few


I am trying to work with the WebAudio API, but have a problem getting it to work properly with Safari. My experiment works properly with Firefox and Chrome.

I have written a Promisified function to play a single note, and then try to play a series of notes using that function.

On Safari only, it fails after the first four notes with the following message:

Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'context.createOscillator')

OK, I haven’t handled the error, but why am I getting it? It suggests a limit of four oscillators.

function tone(frequency,duration) {
	return new Promise(function (resolve,reject) {
		var audioContext = window.AudioContext || window.webkitAudioContext;
		var context=new audioContext;
		var oscillator = context.createOscillator();
		oscillator.frequency.value = frequency;
		oscillator.connect(context.destination);
		oscillator.type='sawtooth';
		oscillator.start(context.currentTime);
		oscillator.stop(context.currentTime+duration);
		oscillator.onended=resolve;
	});
}
document.querySelector('button#play-test').onclick=function(event) {
	tone(130.81,1)
	.then(()=>tone(146.83,1))
	.then(()=>tone(164.81,1))
	.then(()=>tone(174.61,1))
	.then(()=>tone(196.00,1))
	;
};
<button id="play-test">Play</button>


Solution

  • The limit is in the number of AudioContexts you can run simultaneously.

    Some browsers have such a limit, because an AudioContext requires resources from the Hardware (sound card) and that this Hardware has limits.

    So refactor your code so that it doesn't create a new AudioContext every times:

    // create a single audio context
    var context = new (window.AudioContext || window.webkitAudioContext)();
    
    function tone(frequency, duration) {
      return new Promise(function(resolve, reject) {
        var oscillator = context.createOscillator();
        oscillator.frequency.value = frequency;
        oscillator.connect(context.destination);
        oscillator.type = 'sawtooth';
        oscillator.start(context.currentTime);
        oscillator.stop(context.currentTime + duration);
        oscillator.onended = resolve;
      });
    }
    document.querySelector('button#play-test').onclick = function(event) {
      tone(130.81, 1)
        .then(() => tone(146.83, 1))
        .then(() => tone(164.81, 1))
        .then(() => tone(174.61, 1))
        .then(() => tone(196.00, 1))
        .catch(console.error);
    };
    <button id="play-test">Play</button>