CJ Hall
CJ Hall

Reputation: 1

How do I play audio one after another without delay?

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

Answers (3)

zer00ne
zer00ne

Reputation: 44088

Update

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

Jaromanda X
Jaromanda X

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

chrwahl
chrwahl

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

Related Questions