Reputation: 1
I'm attempting to make a game, and in it I have music (like most games). However, changing the track or even looping the same one tends to come with a very slight delay. I want to remove that using pure vanilla Javascript.
I tried this:
// Sets an object with music URLs
const mus = {
'Surface': "https://codeberg.org/NerdB0I/dungeoncrawlerost/raw/branch/main/mus/Surface.wav",
'Deeper': "https://codeberg.org/NerdB0I/dungeoncrawlerost/raw/branch/main/mus/Deeper.wav"
}
// This ensures that music will play and wait for each other using Promises
function musPlay(name = '') {
return new Promise(resolve => {
let audio = new Audio(mus[name]);
audio.onended = () => resolve();
audio.play();
});
}
// This runs once everytime the song ends and plays a new song based on the variable currSong
let currSong = 'Surface';
async function run() {
await musPlay(currSong);
run();
}
run();
// This allows you to press keys to change the song (and to see what key pressed)
// I do not mind that the song continues playing after keydown, I just need it to set the currSong variable for when the files end
window.addEventListener('keydown', (event) => {
if (event.key == 'a') {
currSong = 'Surface';
} else if (event.key == 'd') {
currSong = 'Deeper';
}
document.getElementById('body').innerHTML = event.key;
});
<body id="body">
<p>Click here and then click the "a" and/or "d" keys a couple of times. Give it a second between clicks. This text will be overwritten.</p>
</body>
My Liveweave If this is just a problem of Liveweave and not code, please let me know.
(Additional things I've tried)
I'm beginning to think that it may be a problem of my device. Those of you who answered all seem to say that your code works, yet on my computer it still has the same delay. Would a device cause that latency or what because its not a slow computer generally.
Upvotes: 0
Views: 118
Reputation: 44088
OP experienced the same lag with with my example and surprisingly with Jaromanda X's example as well. So I had my example running flawlessly and now it doesn't even play nor does JX's example ๐. So for what it's worth, I posted my answer as a Plunker
and at the time of this writing it works flawlessly (for how long who knows?). Anyways, if you take a look with the real console you'll probably see a panel full of different errors that I'm pretty sure wasn't our fault.
You should have a function run only once for instantiating the audio
objects. There should either be an audio
object for each file or a single audio
object that you invoke the .load()
method on in order to change src
and reset itself every time you want it play a different file . It appears that you want to do the former (one audio
for each file). But what's happening is you have several audio
that have a 8MB wav file downloaded. Moreover, run()
is recursive but has no exit plan so it continuously runs and calls musPlay()
which always instantiates a new audio object.
function musPlay(name='') {
return new Promise(resolve => {
let audio = new Audio(mus[name]); // ๐ข You need to reference
audio.onended = () => resolve(); // audio only once per file
audio.play();
});
}
async function run() {
await musPlay(currSong); // ๐ข This instantiates another audio.
run(); // ๐ข This loops but there's no way to stop it and each
} // loop eats more resources
When you mentioned that there's a lag between switching files I thought maybe 250ms or something. After tapping the a and d keys several times the file would change after a delay of over 3 to 4 minutes. That delay gets progressively worse.
The example below can play/pause, control volume, and switch audio
objects in both directions. There's 4 MP3s at 800 to 900 KB, small file size means less latency -- WAV files at 8 MB each is not optimal for a looped 51 second background song. Details are commented in example.
// The audio currently active
let current = null,
// The array of audios
nodes = [],
// Counter
idx = 0;
// Endpoints to MP3s
const path = "https://audio.jukehost.co.uk/";
const mp3s = [
`${path}DlrT9JEGEx7c9tiGs0r5WfaUQ2ved12j`,
`${path}BWwEg8iTtfVOUyJpgUbGYqX2UM7IIWqA`,
`${path}po96YoB824SEIYSwcIE9UfxGhIivF3jv`,
`${path}lfW75iB14KrVpqg1tWKpWrAmqsYkW0It`
];
/*
If you want the audios to have any values assigned to their
properties, just add it to cfgX and add the necessary
expression/statement to init()
*/
let cfgX = {
xLoop: true,
xPreload: "auto",
xVolume: 0.7
};
// Populate the nodes array with audios.
const init = async(urls, opts = null) => {
nodes = await Promise.all(urls.map(async(src) => {
const node = await new Audio(src);
node.preload = opts?.xPreload;
node.loop = opts?.xLoop;
node.volume = opts?.xVolume;
return node;
}));
current = nodes[0];
};
// Play/Pause audio
const playSrc = async() => {
if (current.paused || current.ended) {
await current.play();
} else {
current.pause();
}
};
// Volume control
const setGain = (node, delta) => {
let vol = node.volume + delta;
vol = vol < 0 ? 0 : vol > 1 ? 1 : vol;
node.volume = vol;
};
// Load the previous/next audio
const loadSrc = async(dir) => {
let max = nodes.length - 1;
idx = idx + dir;
idx = idx < 0 ? max : idx > max ? 0 : idx;
current.pause();
current.currentTime = 0;
await nodes[idx].play();
current = nodes[idx];
};
// Key Event handler
const audioKeys = (event) => {
event.preventDefault();
const key = event.code;
switch (key) {
case "Space":
playSrc();
break;
case "ArrowUp":
setGain(current, 0.1);
break;
case "ArrowDown":
setGain(current, -0.1);
break;
case "ArrowRight":
loadSrc(1);
break;
case "ArrowLeft":
loadSrc(-1);
break;
default:
break;
}
document.forms.ui.display.value = key;
};
init(mp3s, cfgX);
window.addEventListener("keydown", audioKeys);
:root {
font: 2vmax/1.2 "Segoe UI"
}
main {
display: flex;
align-items: flex-start;
width: 100%;
}
pre {
translate: 0 -1rem;
}
form,
pre {
width: 50%;
min-height: 80vh;
padding: 25px 10px 25px 20px;
border: 2px inset grey;
font-size: 0.9rem;
}
del {
color: red
}
mark {
color: #000;
background: transparent;
}
dl {
margin-top: 20px;
}
dt,
dd {
margin-bottom: 0.5rem;
}
dt,
output {
color: tomato
}
output {
display: inline-block;
width: 8rem;
padding: 4px;
background: gold;
text-align: center;
}
dd {
color: blue;
}
code {
font-size: 1.1rem;
}
<main>
<pre>
๐ WAV files are bloated compared to MP3
# Name Size Duration
---------------------------------------------
<del><mark> 1 Surface.wav 8.60 MB 00:00:51
2 Deeper.wav 8.60 MB 00:00:51</mark></del>
---------------------------------------------
2 WAV Files 17.20 MB 00:01:42
๐ A MP3 is ยนโฑโโ the size of a WAV file
# Name Size Duration
--------------------------------------------
1 Surface.mp3 800 KB 00:00:51
2 Deeper.mp3 800 KB 00:00:51
3 Crystal_Caverns.mp3 924 KB 00:00:39
4 Mysterious_Magic.mp3 831 KB 00:00:35
--------------------------------------------
4 MP3 Files 3.28 MB 00:02:56
</pre>
<form id="ui">
<code><b>event.code</b> = <output id="display"></output></code>
<dl>
<dt><code>Space</code></dt>
<dd>Play/Pause audio</dd>
<dt><code>ArrowUp</code></dt>
<dd>Increase volume</dd>
<dt><code>ArrowDown</code></dt>
<dd>Decrease volume</dd>
<dt><code>ArrowRight</code></dt>
<dd>Play the next audio</dd>
<dt><code>ArrowLeft</code></dt>
<dd>Play the previous audio</dd>
</dl>
</form>
</main>
Upvotes: 0
Reputation: 1
While I agree with with @chrwahl about pre-loading and not creating a new Audio every time, it's not the only cause of the gap. As there is still an amount of time between the ended
event and the starting of the playback, the gap remains
Easiest is to set .loop = true
on the audio, until a different track is required, then set .loop = false
, the ended
will fire when the end is reached, and a new audio track can be started
You'll note this code does not "preload" the audio, yet there is no stutter, because the audio is loaded only when changed
Depending on how much audio you have, I would probably still "pre load" the audio, but that's not important to the issue you have.
const mus = {
'Surface': "https://codeberg.org/NerdB0I/dungeoncrawlerost/raw/branch/main/mus/Surface.wav",
'Deeper': "https://codeberg.org/NerdB0I/dungeoncrawlerost/raw/branch/main/mus/Deeper.wav"
}
const { musPlay, setLoop } = (function () {
let audio;
let playing;
return {
musPlay(name = '') {
return new Promise(resolve => {
playing = name;
audio = new Audio(mus[name]);
audio.onended = resolve;
audio.loop = true;
audio.play();
});
},
setLoop(name) {
if (audio) {
audio.loop = name === playing;
}
}
}
})();
let currSong = 'Surface';
async function run() {
await musPlay(currSong);
run();
}
run();
window.addEventListener('keydown', (event) => {
if (event.key == 'a') {
currSong = 'Surface';
} else if (event.key == 'd') {
currSong = 'Deeper';
}
// this sets the .loop property appropriately
setLoop(currSong);
document.getElementById('body').innerHTML = event.key;
});
<div id="body"></div>
Upvotes: 1
Reputation: 13145
Creating a new audio element each time you play a new audio file will make a pause, because the audio is not loaded. Start the script by loading all the audio files, and then use the same audio element instead of creating ones each time.
// Sets an object with music URLs
let mus = {
'Surface': "https://codeberg.org/NerdB0I/dungeoncrawlerost/raw/branch/main/mus/Surface.wav",
'Deeper': "https://codeberg.org/NerdB0I/dungeoncrawlerost/raw/branch/main/mus/Deeper.wav"
};
// This runs once everytime the song ends and plays a new song based on the variable currSong
let currSong = 'Surface';
let audiopromises = Object.keys(mus).map(key => {
return new Promise(resolve => {
let audio = new Audio();
audio.addEventListener('canplay', e => {
resolve({key: key, elm: e.target});
});
audio.addEventListener('ended', e => {
musPlay();
});
audio.src = mus[key];
});
});
Promise.all(audiopromises).then(audio_arr => {
mus = audio_arr;
musPlay();
});
function musPlay(){
let audio = mus.find(audio_obj => audio_obj.key == currSong);
audio.elm.play();
}
document.addEventListener('keydown', (event) => {
if (event.key == 'a') {
currSong = 'Surface';
} else if (event.key == 'd') {
currSong = 'Deeper';
}
document.body.innerHTML = event.key;
});
Upvotes: 0