Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I play audio files synchronously in JavaScript?

I am working on a program to convert text into morse code audio.

Say I type in sos. My program will turn this into the array [1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]. Where s = dot dot dot (or 1,1,1), and o = dash dash dash (or 2,2,2). This part is quite easy.

Next, I have two sound files:

var dot = new Audio('dot.mp3'); var dash = new Audio('dash.mp3'); 

My goal is to have a function that will play dot.mp3 when it sees a 1, and dash.mp3 when it sees a 2, and pauses when it sees a 0.

The following sort of/ kind of/ sometimes works, but I think it's fundamentally flawed and I don't know how to fix it.

function playMorseArr(morseArr) {   for (let i = 0; i < morseArr.length; i++) {     setTimeout(function() {       if (morseArr[i] === 1) {         dot.play();       }       if (morseArr[i] === 2) {         dash.play();       }     }, 250*i);   } } 

The problem:

I can loop over the array, and play the sound files, but timing is a challenge. If I don't set the setTimeout() interval just right, if the last audio file is not done playing and the 250ms has elapsed, the next element in the array will be skipped. So dash.mp3 is longer than dot.mp3. If my timing is too short, I might hear [dot dot dot pause dash dash pause dot dot dot], or something to that effect.

The effect I want

I want the program to go like this (in pseudocode):

  1. look at the ith array element
  2. if 1 or 2, start playing sound file or else create a pause
  3. wait for the sound file or pause to finish
  4. increment i and go back to step 1

What I have thought of, but don't know how to implement

So the pickle is that I want the loop to proceed synchronously. I've used promises in situations where I had several functions that I wanted executed in a specific order, but how would I chain an unknown number of functions?

I also considered using custom events, but I have the same problem.

like image 373
dactyrafficle Avatar asked Feb 04 '19 03:02

dactyrafficle


People also ask

How do I make audio play in JavaScript?

play() to Play Audio Files in JavaScript. We can load an audio file in JavaScript simply by creating an audio object instance, i.e. using new Audio() . After an audio file is loaded, we can play it using the . play() function.

Can JS play WAV files?

Method 2: JavaScriptYou may also load a sound file with JavaScript, with new Audio() . const audio = new Audio("freejazz. wav"); You may then play back the sound with the .

How do you import an audio file into HTML?

The HTML <audio> element is used to play an audio file on a web page.

What is JavaScript audio?

Audio() The Audio() constructor creates and returns a new HTMLAudioElement which can be either attached to a document for the user to interact with and/or listen to, or can be used offscreen to manage and play audio.


2 Answers

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>
like image 161
Kaiido Avatar answered Oct 06 '22 10:10

Kaiido


Audios 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();       }     });   } } 
like image 45
CertainPerformance Avatar answered Oct 06 '22 09:10

CertainPerformance