Search code examples
javascriptweb-audio-api

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

  • 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.