Play Audio through JS with Overlapping
Asked Answered
C

8

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?

Councilman answered 28/2, 2013 at 0:55 Comment(2)
Ended up going with using the AudioFX library: codeincomplete.com/posts/2011/9/17/revisiting_html5_audioCouncilman
When responsiveness is required, WebAudioContext is the way to go: #44282974Rowlett
T
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.

Transfusion answered 18/8, 2017 at 11:20 Comment(0)
I
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?

Indication answered 4/9, 2017 at 13:24 Comment(1)
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://mcmap.net/q/1158747/-how-to-rapidly-play-multiple-copies-of-a-soundfile-in-javascript 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. codepen.io/SitePoint/pen/JRaLVRExquisite
J
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.

Jalap answered 28/2, 2013 at 1:4 Comment(4)
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.Councilman
My usual solution to this is to build an array. I'll edit my answer with that.Jalap
Doesn't seem to work but I get the rough idea of what you're trying to do.Councilman
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 problemCouncilman
K
1

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();
    }
}
Kier answered 12/2, 2018 at 11:5 Comment(1)
thank you for your feedback. I changed the variable names as you suggested. c stands for counterKier
G
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
Gel answered 8/4, 2020 at 9:1 Comment(0)
I
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()
Inhibition answered 10/7, 2022 at 4:55 Comment(0)
C
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;
    }
  }
Californium answered 26/8, 2022 at 0:3 Comment(1)
Sorry I accidently posted it uncompleted and scrammed to fix it, I am going to clean it upCalifornium
G
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()

}

Gummosis answered 5/9, 2022 at 19:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.