'Web Audio API sound interaction stops working after a while

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;


Solution 1:[1]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1