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();
})
audio
tag? I assume that's how it's preloaded? – Miracidium