I'm working with the Web Audio API for my javascript project, and I've run into an issue that I can't seem to find the answer for anywhere.
I've added event listeners to respond to keydown events- every time a user presses a certain key on their keyboard, a sound will play. This works for a little while, but after maybe around 6 seconds of pressing keys, something happens that makes the sound stop - the keys won't produce sound for maybe half a second, then they will start working again. Anyone have any idea why this is happening, and how I can fix it?
Here's my code for the event listener :
import Audio from './scripts/audio'
document.addEventListener('keydown', (e) => {
const audio = new Audio();
let key = e.key;
audio.createNotes(key);
})
and here's my code for the audio :
class Audio {
constructor() {
// instantiate web audio api object
this.audioContext = new AudioContext();
// create gain node, gain corresponds with volume
this.gainNode = this.audioContext.createGain();
this.gainNode.gain.setValueAtTime(0.08, 0);
// allows volume to decrease with time
this.gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + 1.5);
}
createNotes(key) {
// C4 to C5 scale, attach frequencies to corresponding keyboard value
const notes = {
's': 261.63,
'd': 293.66,
'f': 329.63,
'g': 349.23,
'h': 392.00,
'j': 440.00,
'k': 493.88,
'l': 523.25,
'e': 587.33,
'r': 659.25,
't': 698.46,
'y': 783.99,
'u': 880.00,
'i': 987.77,
'o': 1046.50,
'p': 1174.66
}
// if e.key corresponds with notes key, we want to play sound
if (notes[key]) {
// oscillator corresponds with frequency,
// create oscillator node to attach frequency from notes object
let oscillator = this.audioContext.createOscillator();
oscillator.frequency.setValueAtTime(notes[key], this.audioContext.currentTime);
// lower gain for higher frequency notes
if (notes[key] > 699) {
this.gainNode.gain.setValueAtTime(0.03, this.audioContext.currentTime);
}
// connect oscillator node to volume node
oscillator.connect(this.gainNode);
// connect gain node to destination (speakers)
this.gainNode.connect(this.audioContext.destination);
oscillator.start(0);
// tone will play for 1.5 seconds
oscillator.stop(this.audioContext.currentTime + 1.5)
}
}
}
export default Audio;
The problem is that you're creating too many AudioContext instances. This is not the intended usage of the API. Why are you creating so many instances? You should re-use them.
Generally you should only need a single AudioContext. On the mozzila developer page it's clearly stated that some Chrome versions only support 6.
https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/AudioContext#google_chrome
Here's a question/answer that further explains the problem. Chrome produces no audio after reaching 50 audio output streams
To solve your problem basically create a single AudioContext and make it accessible globally like so:
let globalAudioContext = new AudioContext();
class Audio
{
constructor()
{
// instantiate web audio api object
// create gain node, gain corresponds with volume
this.gainNode = globalAudioContext.createGain();
this.gainNode.gain.setValueAtTime(0.08, 0);
// allows volume to decrease with time
this.gainNode.gain.exponentialRampToValueAtTime(0.001, globalAudioContext.currentTime + 1.5);
}
createNotes(key)
{
// C4 to C5 scale, attach frequencies to corresponding keyboard value
const notes = {
's': 261.63,
'd': 293.66,
'f': 329.63,
'g': 349.23,
'h': 392.00,
'j': 440.00,
'k': 493.88,
'l': 523.25,
'e': 587.33,
'r': 659.25,
't': 698.46,
'y': 783.99,
'u': 880.00,
'i': 987.77,
'o': 1046.50,
'p': 1174.66
}
// if e.key corresponds with notes key, we want to play sound
if (notes[key]) {
// oscillator corresponds with frequency,
// create oscillator node to attach frequency from notes object
let oscillator = globalAudioContext.createOscillator();
oscillator.frequency.setValueAtTime(notes[key], globalAudioContext.currentTime);
// lower gain for higher frequency notes
if (notes[key] > 699) {
this.gainNode.gain.setValueAtTime(0.03, globalAudioContext.currentTime);
}
// connect oscillator node to volume node
oscillator.connect(this.gainNode);
// connect gain node to destination (speakers)
this.gainNode.connect(globalAudioContext.destination);
oscillator.start(0);
// tone will play for 1.5 seconds
oscillator.stop(globalAudioContext.currentTime + 1.5);
}
}
}
document.addEventListener('keydown', (e) =>
{
const audio = new Audio();
let key = e.key;
console.log(e.keyCode);
audio.createNotes(key);
})
If you're trying to make a keyboard piano, you should have each key bound to a pre-initialized graph node (Gain Node) and reuse them.
And the reason why it starts working again is because it takes time for the garbage collector to kick in.