11

I have the following JS code for a canvas based game.

var EXPLOSION = "sounds/explosion.wav";

function playSound(str, vol) {
  var snd = new Audio();
  snd.src = str;
  snd.volume = vol;
  snd.play();
}

function createExplosion() {
  playSound(EXPLOSION, 0.5);
}

This works, however it sends a server request to download the sound file every time it is called. Alternatively, if I declare the Audio object beforehand:

var snd = new Audio();
snd.src = EXPLOSION;
snd.volume = 0.5;

function createExplosion() {
  snd.play();
}

This works, however if the createExplosion function is called before the sound is finished playing, it does not play the sound at all. This means that only a single playthrough of the sound file is allowed at a time - and in scenarios that multiple explosions are taking place it doesn't work at all.

Is there any way to properly play an audio file multiple times overlapping with itself?

Lucas Penney
  • 2,624
  • 4
  • 27
  • 36
  • 1
    Ended up going with using the AudioFX library: http://codeincomplete.com/posts/2011/9/17/revisiting_html5_audio/ – Lucas Penney Feb 28 '13 at 03:25
  • When responsiveness is required, WebAudioContext is the way to go: https://stackoverflow.com/questions/44282474/html-canvas-javascript-triggering-audio-by-selection-from-multiple-places/44289845#44289845 – Kaiido Jul 22 '18 at 06:16

8 Answers8

2

I was looking for this for ages in a tetris game i'm building and I think this solution is the best.

function playSoundMove() {
  var sound = document.getElementById("move");
  sound.load();     
  sound.play();
}

just have it loaded and ready to go.

alex
  • 5,467
  • 4
  • 33
  • 43
2

You could just duplicate the node with cloneNode() and play() that duplicate node.

My audio element looks like this:

<audio id="knight-audio" src="knight.ogg" preload="auto"></audio>

and I have an onClick listener that does just that:

function click() {
    const origAudio = document.getElementById("knight-audio");
    const newAudio = origAudio.cloneNode()
    newAudio.play()
}

And since the audio element isn't going to be displayed, you don't actually have to attach the node to anything.

I verified client-side and server-side that Chrome only tries to download the audio file once.

Caveats: I'm not sure about performance impacts, since this on my site this clip doesn't get played more than ~40x maximum for a page. You might have to clean up the audio nodes if you're doing something much larger than that?

Roger Filmyer
  • 676
  • 1
  • 8
  • 24
  • THIS IS A BAD ANSWER. Cloning the audio node like this will fill up memory and eventually audio will just completely stop working on your webpage until you restart the browser. https://stackoverflow.com/a/61457101/4111447 You should instead use the AudioContext API which was actually made for doing this and is way faster/ more efficient at playing a sound repeatably, and overlapping. https://codepen.io/SitePoint/pen/JRaLVR – Tomer Shemesh Feb 17 '23 at 16:00
1

Try this:

(function() {
    var snds = {};
    window.playSound(str,vol) {
        if( !snds[str]) (snds[str] = new Audio()).src = str;
        snds[str].volume = vol;
        snds[str].play();
    }
})();

Then the first time you call it it will fetch the sound, but every time after that it will reuse the same sound object.


EDIT: You can also preload with duplicates to allow the sound to play more than once at a time:

(function() {
    var snds = {}
    window.playSound = function(str,vol) {
        if( !snds[str]) {
            snds[str] = [new Audio()];
            snds[str][0].src = str;
        }
        var snd = snds[str], pointer = 0;
        while( snd[pointer].playing) {
            pointer++;
            if( pointer >= snd.length) {
                snd.push(new Audio());
                snd[pointer].src = str;
            }
        }
        snd[pointer].volume = vol;
        snd[pointer].play();
    };
})();

Note that this will send multiple requests if you play the sound overlapping itself too much, but it should return Not Modified very quickly and will only do so if you play it more times than you have previously.

Niet the Dark Absol
  • 320,036
  • 81
  • 464
  • 592
  • This suffers the same problem. The sound will play, but attempting to play it again before a previous instance has finished playing results in it not playing the sound at all. It seems to be a problem with play()ing an audio object before it's finished. – Lucas Penney Feb 28 '13 at 01:11
  • My usual solution to this is to build an array. I'll edit my answer with that. – Niet the Dark Absol Feb 28 '13 at 01:12
  • Doesn't seem to work but I get the rough idea of what you're trying to do. – Lucas Penney Feb 28 '13 at 01:27
  • Still only gets one sound at a time - it looks like it's only creating a single audio object per sound string even though it should create more, resulting in the same problem – Lucas Penney Feb 28 '13 at 01:43
0

Relying more on memory than process time, we can make an array of multiple clones of the Audio and then play them by order:

function gameSnd() {
    tick_wav = new Audio('sounds/tick.wav');
    victory_wav = new Audio('sounds/victory.wav');
    counter = 0;
    ticks = [];

    for (var i = 0; i<10;i++)
        ticks.push(tick_wav.cloneNode());

    tick = function(){
        counter = (counter + 1)%10;
        ticks[counter].play();
    }

    victory = function(){
        victory_wav.play();
    }
}
  • 2
    You should really use better variable names than just `t`, `c` and `v`, as they give me no clue what they are about. Something like `victorySound`, and `tickSound` are so much better. – AustinWBryan Jul 22 '18 at 05:52
  • I agree with Austin. This is difficult to read, and I shouldn't have to ask for clarification to know what `c` is (btw, what is `c`?). – aggregate1166877 Jul 15 '22 at 11:27
  • thank you for your feedback. I changed the variable names as you suggested. `c` stands for counter – Bashir Abdelwahed Jul 18 '22 at 17:14
0

In my game i'm using preoading but after the sound is initiated (its not so smart to not preload at all or preload everything on page load, some sound hasn't played in some gameplay at all, why to load them)

const audio {};
audio.dataload = {'entity':false,'entityes':[],'n':0};
audio.dataload.ordernum = function() {
  audio.dataload.n = (audio.dataload.n + 1)%10;
  return audio.dataload.n;
}
audio.dataload.play =  function() {
 
    audio.dataload.entity = new Audio('/some.mp3');
    for (let i = 0; i<10;i++) {
      audio.dataload.entityes.push(audio.dataload.entity.cloneNode());
    }
   
    audio.dataload.entityes[audio.dataload.ordernum()].play();
  
}

audio.dataload.play() // plays sound and preload sounds to memory when it isn't
Martin
  • 2,575
  • 6
  • 32
  • 53
0

I've created a class that allows for layered audio. This is very similar to other answers where it creates another node with the same src, but this class will only do that if necessary. If it has created a node already that has been completed, it will replay that existing node.

Another tweak to this is that initially fetch the audio and use the URL of the blob. I do this for efficiency; so the src doesn't have to be fetched externally every single time a new node is created.

class LayeredAudio {

    url;
    samples = [];

    constructor(src){
        fetch(src)
            .then(response => response.blob())
            .then((blob) => {
                this.url = URL.createObjectURL(blob);
                this.samples[0] = new Audio(this.url);
            });
    }

    play(){
        if(!this.samples.find(e => e.paused)?.play()){
            this.samples.push(new Audio(this.url))
            this.samples[this.samples.length - 1].play()
        }
    }
}
  
const aud = new LayeredAudio("URL");
aud.play()
Diriector_Doc
  • 582
  • 1
  • 12
  • 28
0

When I tried some of the other solutions there was some delay, but I may have found a better alternative. This will plow through a good chunk of memory if you make the audio array's length high. I doubt you will need to play the same audio more than 10 times at the same time, but if you do just make the array length longer.

  var audio = new Array(10); 
  // The length of the audio array is how many times
  // the audio can overlap
  for (var i = 0; i < audio.length; i++) {
    audio[i] = new Audio("your audio");
  }
  function PlayAudio() {
    // Whenever you want to play it call this function
    audio[audioIndex].play();
    audioIndex++;
    if(audioIndex > audio.length - 1) {
      audioIndex = 0;
    }
  }
hotpig
  • 3
  • 3
0

I have found this to be the simples way to overlap the same audio over itself

<button id="btn" onclick="clickMe()">ding</button>

<script>

function clickMe() {
    const newAudio = new Audio("./ding.mp3")
    newAudio.play()

}