HTML5 audio - play sound repeatedly on click, regardless if previous iteration has finished
Asked Answered
M

4

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?

Miracidium answered 31/7, 2011 at 23:42 Comment(0)
W
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();
});
Waldron answered 1/8, 2011 at 0:20 Comment(7)
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?Miracidium
Hah all fears debunked, thanks! Now when will JQuery start supporting these functions so I can do $('selector').play() and whatnot?Miracidium
This is an overkill. Just set currentTime to 0: https://mcmap.net/q/582863/-html-audio-element-how-to-replayCerenkov
@AllenWebguy You can do it in jQuery with $('selector')[0].play();Dock
@Cerenkov that would cut off the sound though.Waldron
Hey @Waldron thanks! Just one question, should we remove the EventListener as well, or should it be fine? (memory leak?)Maracaibo
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?)Maracaibo
A
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();
}
Antonetteantoni answered 23/9, 2012 at 18:34 Comment(6)
This is great. Is there anyway to do the same thing but using multiple mp3's?Sovran
@Antonetteantoni This is nice, but each new click add's a HTTP request :(Maracaibo
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 firingFiertz
Hi @jmservera, I wondering if this clone approach can cause memory leaks? Because the cloned element never be cleaned right?Guggle
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 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.Antonetteantoni
@Antonetteantoni But is there a less ancient workaround that doesn't involve using external libraries?Shea
W
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();
});
Waldron answered 1/8, 2011 at 0:20 Comment(7)
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?Miracidium
Hah all fears debunked, thanks! Now when will JQuery start supporting these functions so I can do $('selector').play() and whatnot?Miracidium
This is an overkill. Just set currentTime to 0: https://mcmap.net/q/582863/-html-audio-element-how-to-replayCerenkov
@AllenWebguy You can do it in jQuery with $('selector')[0].play();Dock
@Cerenkov that would cut off the sound though.Waldron
Hey @Waldron thanks! Just one question, should we remove the EventListener as well, or should it be fine? (memory leak?)Maracaibo
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?)Maracaibo
D
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>
Dock answered 2/11, 2012 at 9:59 Comment(1)
does preload="preload" load the audio before clicking on the play button in HTML5 ?Hurdygurdy
C
1

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();
})
Ceporah answered 16/8, 2023 at 20:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.