How do I play audio files synchronously in JavaScript?
Do not use HTMLAudioElement for that kind of application.
The HTMLMediaElements are by nature asynchronous and everything from the play()
method to the pause()
one and going through the obvious resource fetching and the less obvious currentTime
setting is asynchronous.
This means that for applications that need perfect timings (like a Morse-code reader), these elements are purely unreliable.
Instead, use the Web Audio API, and its AudioBufferSourceNodes objects, which you can control with µs precision.
First fetch all your resources as ArrayBuffers, then when needed generate and play AudioBufferSourceNodes from these ArrayBuffers.
You'll be able to start playing these synchronously, or to schedule them with higher precision than setTimeout will offer you (AudioContext uses its own clock).
Worried about memory impact of having several AudioBufferSourceNodes playing your samples? Don't be. The data is stored only once in memory, in the AudioBuffer. AudioBufferSourceNodes are just views over this data and take up no place.
// I use a lib for Morse encoding, didn't tested it too much though// https://github.com/Syncthetic/MorseCode/const morse = Object.create(MorseCode);const ctx = new (window.AudioContext || window.webkitAudioContext)();(async function initMorseData() { // our AudioBuffers objects const [short, long] = await fetchBuffers(); btn.onclick = e => { let time = 0; // a simple time counter const sequence = morse.encode(inp.value); console.log(sequence); // dots and dashes sequence.split('').forEach(type => { if(type === ' ') { // space => 0.5s of silence time += 0.5; return; } // create an AudioBufferSourceNode let source = ctx.createBufferSource(); // assign the correct AudioBuffer to it source.buffer = type === '-' ? long : short; // connect to our output audio source.connect(ctx.destination); // schedule it to start at the end of previous one source.start(ctx.currentTime + time); // increment our timer with our sample's duration time += source.buffer.duration; }); }; // ready to go btn.disabled = false})() .catch(console.error);function fetchBuffers() { return Promise.all( [ 'https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3', 'https://dl.dropboxusercontent.com/s/h2j6vm17r07jf03/snare.mp3' ].map(url => fetch(url) .then(r => r.arrayBuffer()) .then(buf => ctx.decodeAudioData(buf)) ) );}
<script src="https://cdn.jsdelivr.net/gh/mohayonao/promise-decode-audio-data@eb4b1322113b08614634559bc12e6a8163b9cf0c/build/promise-decode-audio-data.min.js"></script><script src="https://cdn.jsdelivr.net/gh/Syncthetic/MorseCode@master/morsecode.js"></script><input type="text" id="inp" value="sos"><button id="btn" disabled>play</button>
Audio
s have an ended
event that you can listen for, so you can await
a Promise
that resolves when that event fires:
const audios = [undefined, dot, dash];async function playMorseArr(morseArr) { for (let i = 0; i < morseArr.length; i++) { const item = morseArr[i]; await new Promise((resolve) => { if (item === 0) { // insert desired number of milliseconds to pause here setTimeout(resolve, 250); } else { audios[item].onended = resolve; audios[item].play(); } }); }}
I will use a recursive approach that will listen on the audio ended event. So, every time the current playing audio stop, the method is called again to play the next one.
function playMorseArr(morseArr, idx){ // Finish condition. if (idx >= morseArr.length) return; let next = function() {playMorseArr(morseArr, idx + 1)}; if (morseArr[idx] === 1) { dot.onended = next; dot.play(); } else if (morseArr[idx] === 2) { dash.onended = next; dash.play(); } else { setTimeout(next, 250); }}
You can initialize the procedure calling playMorseArr()
with the array and the start index:
playMorseArr([1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1], 0);
A test example (Using the dummy mp3
files from Kaiido's answer)
let [dot, dash] = [ new Audio('https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3'), new Audio('https://dl.dropboxusercontent.com/s/h2j6vm17r07jf03/snare.mp3')];function playMorseArr(morseArr, idx){ // Finish condition. if (idx >= morseArr.length) return; let next = function() {playMorseArr(morseArr, idx + 1)}; if (morseArr[idx] === 1) { dot.onended = next; dot.play(); } else if (morseArr[idx] === 2) { dash.onended = next; dash.play(); } else { setTimeout(next, 250); }}playMorseArr([1,1,1,0,2,2,2,0,1,1,1], 0);