24

I'm building a game in which a wav file plays on click - in this case it's a gun sound "bang".

The problem is if I rapid click, it won't play the sound once for each click - it's as if the clicks are ignored while the sound is playing, and once the sound is finished, it starts listening for clicks again. The delay seems to be about one second long, so you figure if someone clicks 4 or 5 times per second, I want 5 bangs, not 1.

Here's my HTML:

<audio id="gun_sound" preload>
    <source src="http://www.seancannon.com/_test/audio/gun_bang.wav" />
</audio>

Here's my JS:

$('#' + CANVAS_ID).bind(_click, function() {
    document.getElementById('gun_sound').play();
    adj_game_data(GAME_DATA_AMMO_ID, -1);
    check_ammo();
}); 

Ideas?

Danziger
  • 19,628
  • 4
  • 53
  • 83
AlienWebguy
  • 76,997
  • 17
  • 122
  • 145

4 Answers4

20

I know it's a very late answer, I just wanted to , but seeking for a solution I found that in my case the best one was to preload the sound and then clone the node each time I want to play it, this allows me to play it even if the previous has not ended:

var sound = new Audio("click.mp3");
sound.preload = 'auto';
sound.load();

function playSound(volume) {
  var click=sound.cloneNode();
  click.volume=volume;
  click.play();
}
jmservera
  • 6,454
  • 2
  • 32
  • 45
  • This is great. Is there anyway to do the same thing but using multiple mp3's? – gravityboy Sep 04 '13 at 06:51
  • @jmservera This is nice, but each new click add's a HTTP request :( – NiCk Newman Aug 31 '15 at 12:14
  • Works like a charm in Chrome, but for some reason Firefox doesn't play. I do see the speaker icon on the firefox tab, so there is audio, and if I have ANY other audio playing from my PC (youtube video, skype call, etc.) then Firefox even plays it audibly - otherwise I cannot hear it :-/ any ideas? I'm shooting out many clones via a for loop, so think of it as a machine gun firing – Beezle-Bug Apr 22 '20 at 08:21
  • Hi @jmservera, I wondering if this clone approach can cause memory leaks? Because the cloned element never be cleaned right? – mupinnn Nov 06 '21 at 05:28
  • Hi @mupinnn. As you can see, this is an ancient workaround (from 10 years ago) that shouldn't be used in newer browsers; instead, use modern apis or something like https://howlerjs.com/. However, answering to your question: the variable is local to the function and the element is not appended to the document. Furthermore, it is replaced with each clone, so, in theory, the garbage collector should pick away any unused clones. – jmservera Mar 28 '22 at 08:38
  • @jmservera But is there a less ancient workaround that doesn't involve using external libraries? – Trever Thompson Feb 02 '23 at 21:33
16

Once the gun_bang.wav is preloaded, you can dynamically make new elements with that sound attached to it, play the audio, and then remove them when the audio has finished.

function gun_bang() {
  var audio = document.createElement("audio");
  audio.src = "http://www.seancannon.com/_test/audio/gun_bang.wav";
  audio.addEventListener("ended", function() {
    document.removeChild(this);
  }, false);
  audio.play();
}

$('#' + CANVAS_ID).bind(_click, function() {
  gun_bang();
  adj_game_data(GAME_DATA_AMMO_ID, -1);
  check_ammo();
});
mplungjan
  • 169,008
  • 28
  • 173
  • 236
Maverick
  • 3,039
  • 6
  • 26
  • 35
  • This looks promising. My only fear is that there will be latency between the click and the audio while the element is being created. Do I still need my initial `audio` tag? I assume that's how it's preloaded? – AlienWebguy Aug 01 '11 at 00:22
  • Hah all fears debunked, thanks! Now when will JQuery start supporting these functions so I can do `$('selector').play()` and whatnot? – AlienWebguy Aug 01 '11 at 00:31
  • 2
    This is an overkill. Just set `currentTime` to `0`: http://stackoverflow.com/a/7005562/352796 – katspaugh Dec 22 '11 at 15:05
  • 2
    @AllenWebguy You can do it in jQuery with `$('selector')[0].play();` – Volomike Nov 02 '12 at 09:39
  • 2
    @katspaugh that would cut off the sound though. – Maverick Aug 08 '13 at 21:16
  • Hey @Maverick thanks! Just one question, should we remove the EventListener as well, or should it be fine? (memory leak?) – NiCk Newman Aug 31 '15 at 11:52
  • Actually, this method creates a new request each time which is not good. Is there a way to check if it's already loaded and still play repeatedly upon clicking? (without making a new request each click?) – NiCk Newman Aug 31 '15 at 11:56
1

You can wrap the following with your own program-specific click logic, but the following demonstrates 3 gun sounds at the same time. If you want any kind of delay, you can introduce a minor sleep() function (many kludge hacks for this are easily found), or you can use setTimeout() to call subsequent ones.

<audio id="gun_sound" preload="preload">
    <source src="http://www.seancannon.com/_test/audio/gun_bang.wav" />
</audio>

<script type="text/javascript">
jQuery(document).ready(function($){

  $('#gun_sound').clone()[0].play();
  $('#gun_sound').clone()[0].play();
  $('#gun_sound').clone()[0].play();

});
</script>
Volomike
  • 23,743
  • 21
  • 113
  • 209
0

The HTML5 audio element has many limitations and it's not well suited for a complex audio use-case like a game.

Instead, consider using the WebAudio API. You'll have to:

  • Use a XMLHttpRequest to load your audio file in a buffer

  • Assign that buffer to a AudioBufferSourceNode instance.

  • Connect that node to the appropriate destination or intermediate nodes (e.g. GainNode).

  • If you also need to stop sounds, you'll need to keep track of each source node that is created and that hasn't finished playing yet, which can be done by listen to the source node's onended.

Here's a class that encapsulates the logic to load a sound from a URL and play/stop it as many times as you want, keeping track of all currently playing sources and cleaning them up as needed:

window.AudioContext = window.AudioContext || window.webkitAudioContext;

const context = new AudioContext();

export class Sound {

    url = '';

    buffer = null;

    sources = [];

    constructor(url) {
        this.url = url;
    }

    load() {
        if (!this.url) return Promise.reject(new Error('Missing or invalid URL: ', this.url));

        if (this.buffer) return Promise.resolve(this.buffer);

        return new Promise((resolve, reject) => {
            const request = new XMLHttpRequest();

            request.open('GET', this.url, true);
            request.responseType = 'arraybuffer';

            // Decode asynchronously:

            request.onload = () => {
                context.decodeAudioData(request.response, (buffer) => {
                    if (!buffer) {
                        console.log(`Sound decoding error: ${ this.url }`);

                        reject(new Error(`Sound decoding error: ${ this.url }`));

                        return;
                    }

                    this.buffer = buffer;

                    resolve(buffer);
                });
            };

            request.onerror = (err) => {
                console.log('Sound XMLHttpRequest error:', err);

                reject(err);
            };

            request.send();
        });
    }

    play(volume = 1, time = 0) {
        if (!this.buffer) return;

        // Create a new sound source and assign it the loaded sound's buffer:

        const source = context.createBufferSource();

        source.buffer = this.buffer;

        // Keep track of all sources created, and stop tracking them once they finish playing:

        const insertedAt = this.sources.push(source) - 1;

        source.onended = () => {
            source.stop(0);

            this.sources.splice(insertedAt, 1);
        };

        // Create a gain node with the desired volume:

        const gainNode = context.createGain();

        gainNode.gain.value = volume;

        // Connect nodes:

        source.connect(gainNode).connect(context.destination);

        // Start playing at the desired time:

        source.start(time);
    }

    stop() {
        // Stop any sources still playing:

        this.sources.forEach((source) => {
            source.stop(0);
        });

        this.sources = [];
    }

}

You can then do something like this:

const soundOne = new Sound('./sounds/sound-one.mp3')
const soundTwo = new Sound('./sounds/sound-two.mp3')

Promises.all([
  soundOne.load(),
  soundTwo.load(),
]).then(() => {
  buttonOne.onclick = () => soundOne.play();
  buttonTwo.onclick = () => soundOne.play();
})
Danziger
  • 19,628
  • 4
  • 53
  • 83