web-dev-qa-db-fra.com

Comment lire des fichiers audio de manière synchrone en JavaScript?

Je travaille sur un programme pour convertir du texte en audio morse.

Disons que je tape sos. Mon programme transformera cela en tableau [1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]. Où s = dot dot dot (Ou 1,1,1) Et o = dash dash dash (Ou 2,2,2). Cette partie est assez simple.

Ensuite, j'ai deux fichiers son:

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

Mon objectif est d'avoir une fonction qui jouera dot.mp3 Quand elle verra un 1, Et dash.mp3 Quand elle verra un 2, Et fera une pause quand elle verra un 0.

Le type suivant de/genre de/fonctionne parfois, mais je pense qu'il est fondamentalement défectueux et je ne sais pas comment le corriger.

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);
  }
}

Le problème:

Je peux parcourir le tableau et lire les fichiers audio, mais le timing est un défi. Si je ne règle pas correctement l'intervalle setTimeout(), si la lecture du dernier fichier audio n'est pas terminée et que 250ms S'est écoulé, l'élément suivant du tableau sera ignoré. Donc dash.mp3 Est plus long que dot.mp3. Si mon timing est trop court, je pourrais entendre [dot dot dot pause dash dash pause dot dot dot], Ou quelque chose dans ce sens.

L'effet que je veux

Je veux que le programme se passe comme ça (en pseudocode):

  1. regardez l'élément de tableau ith
  2. si 1 ou 2, lancez la lecture du fichier audio ou créez une pause
  3. attendez que le fichier son ou pause pour terminer
  4. incrémenter i et revenir à étape 1

Ce à quoi j'ai pensé, mais je ne sais pas comment l'implémenter

Donc, le cornichon est que je veux que la boucle se déroule de manière synchrone. J'ai utilisé des promesses dans des situations où j'avais plusieurs fonctions que je voulais exécuter dans un ordre spécifique, mais comment chaîner un nombre inconnu de fonctions?

J'ai également envisagé d'utiliser des événements personnalisés, mais j'ai le même problème.

44
dactyrafficle

N'utilisez pas HTMLAudioElement pour ce type d'application.

Les HTMLMediaElements sont par nature asynchrones et tout, de la méthode play() à pause() et passant par l'extraction de ressources évidente et le paramètre currentTime moins évident est asynchrone.

Cela signifie que pour les applications qui nécessitent des synchronisations parfaites (comme un lecteur de code Morse), ces éléments sont purement peu fiables.

Utilisez plutôt l'API Web Audio et ses objets AudioBufferSourceNode s, que vous pouvez contrôler avec une précision de µs.

Commencez par récupérer toutes vos ressources en tant que ArrayBuffers, puis si nécessaire, générez et lisez AudioBufferSourceNodes à partir de ces ArrayBuffers.

Vous pourrez commencer à les lire de manière synchrone ou à les planifier avec une précision supérieure à ce que setTimeout vous offrira (AudioContext utilise sa propre horloge).

Vous vous inquiétez de l'impact sur la mémoire de la présence de plusieurs AudioBufferSourceNodes dans vos échantillons? Ne le sois pas. Les données ne sont stockées qu'une seule fois en mémoire, dans l'AudioBuffer. AudioBufferSourceNodes sont juste des vues sur ces données et ne prennent aucune 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>
44
Kaiido

Audios ont un événement ended que vous pouvez écouter, vous pouvez donc await un Promise qui se résout lorsque cet événement se déclenche:

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();
      }
    });
  }
}
18
CertainPerformance

J'utiliserai une approche récursive qui écoutera sur l'événement audio terminé . Ainsi, chaque fois que la lecture audio en cours s'arrête, la méthode est appelée à nouveau pour lire la suivante.

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);
    }
}

Vous pouvez initialiser la procédure appelant playMorseArr() avec le tableau et l'index de démarrage:

playMorseArr([1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1], 0);

Un exemple de test (Utilisation des fichiers factices mp3 De Réponse de Kaiido )

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);
10
Shidersz

async & await

Bien qu'ils soient utilisés pour des opérations asynchrones, ils peuvent également être utilisés pour des tâches synchrones. Vous faites une promesse pour chaque fonction, enveloppez-les dans un async function, puis appelez-les avec await un par un. Ce qui suit est la documentation du async function en tant que fonction nommée dans la démo, celle de la démo réelle est une fonction fléchée mais dans les deux cas, elles sont identiques:

 /**
  * async function sequencer(seq, t)
  *
  * @param {Array} seq - An array of 0s, 1s, and 2s. Pause. Dot, and Dash respectively.
  * @param {Number} t - Number representing the rate in ms.
  */

Plunker

Démo

Remarque: Si l'extrait de pile ne fonctionne pas, consultez le Plunker

<!DOCTYPE html>
<html>

<head>
  <style>
    html,
    body {
      font: 400 16px/1.5 Consolas;
    }
    
    fieldset {
      max-width: fit-content;
    }
    
    button {
      font-size: 18px;
      vertical-align: middle;
    }
    
    #time {
      display: inline-block;
      width: 6ch;
      font: inherit;
      vertical-align: middle;
      text-align: center;
    }
    
    #morse {
      display: inline-block;
      width: 30ch;
      margin-top: 0px;
      font: inherit;
      text-align: center;
    }
    
    [name=response] {
      position: relative;
      left: 9999px;
    }
  </style>
</head>

<body>
  <form id='main' action='' method='post' target='response'>
    <fieldset>
      <legend>Morse Code</legend>
      <label>Rate:
        <input id='time' type='number' min='300' max='1000' pattern='[2-9][0-9]{2,3}' required value='350'>ms
      </label>
      <button type='submit'>
        ????➖
      </button>
      <br>
      <label><small>0-Pause, 1-Dot, 2-Dash (no delimiters)</small></label>
      <br>
      <input id='morse' type='number' min='0' pattern='[012]+' required value='111000222000111'>
    </fieldset>
  </form>
  <iframe name='response'></iframe>
  <script>
    const dot = new Audio(`https://od.lk/s/NzlfOTYzMDgzN18/dot.mp3`);
    const dash = new Audio(`https://od.lk/s/NzlfOTYzMDgzNl8/dash.mp3`);

    const sequencer = async(array, FW = 350) => {

      const pause = () => {
        return new Promise(resolve => {
          setTimeout(() => resolve(dot.pause(), dash.pause()), FW);
        });
      }
      const playDot = () => {
        return new Promise(resolve => {
          setTimeout(() => resolve(dot.play()), FW);
        });
      }
      const playDash = () => {
        return new Promise(resolve => {
          setTimeout(() => resolve(dash.play()), FW + 100);
        });
      }

      for (let seq of array) {
        if (seq === 0) {
          await pause();
        }
        if (seq === 1) {
          await playDot();
        }
        if (seq === 2) {
          await playDash();
        }
      }
    }

    const main = document.forms[0];
    const ui = main.elements;

    main.addEventListener('submit', e => {
      let t = ui.time.valueAsNumber;
      let m = ui.morse.value;
      let seq = m.split('').map(num => Number(num));
      sequencer(seq, t);
    });
  </script>
</body>

</html>
1
zer00ne