it-swarm.com.de

Wie spiele ich Audiodateien synchron in JavaScript ab?

Ich arbeite an einem Programm, um Text in Morsecode-Audio umzuwandeln.

Angenommen, ich tippe sos ein. Mein Programm verwandelt dies in das Array [1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]. Wobei s = dot dot dot (Oder 1,1,1) Und o = dash dash dash (Oder 2,2,2). Dieser Teil ist ganz einfach.

Als nächstes habe ich zwei Sounddateien:

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

Mein Ziel ist es, eine Funktion zu haben, die dot.mp3 Spielt, wenn ein 1 Und dash.mp3, Wenn ein 2 Angezeigt wird, und pausiert, wenn sie angezeigt wird a 0.

Die folgende Art von/Art von/funktioniert manchmal, aber ich denke, es ist grundlegend fehlerhaft und ich weiß nicht, wie ich es beheben soll.

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

Das Problem:

Ich kann das Array durchlaufen und die Sounddateien abspielen, aber das Timing ist eine Herausforderung. Wenn ich das Intervall setTimeout() nicht genau richtig einstelle, wenn die letzte Audiodatei nicht abgespielt wird und 250ms Abgelaufen ist, wird das nächste Element im Array übersprungen. dash.mp3 Ist also länger als dot.mp3. Wenn mein Timing zu kurz ist, höre ich möglicherweise [dot dot dot pause dash dash pause dot dot dot] Oder etwas in diesem Sinne.

Der Effekt, den ich will

Ich möchte, dass das Programm so läuft (im Pseudocode):

  1. schauen Sie sich das Array-Element ith an
  2. wenn 1 oder 2, starten Sie die Wiedergabe der Audiodatei oder erstellen Sie eine Pause
  3. warten Sie, bis die Audiodatei oder Pause beendet ist
  4. inkrementiere i und gehe zurück zu Schritt 1

Was ich gedacht habe, aber nicht weiß, wie ich es implementieren soll

Das Problem ist also, dass die Schleife synchron ablaufen soll. Ich habe Versprechen in Situationen verwendet, in denen mehrere Funktionen, die ich ausführen wollte, in einer bestimmten Reihenfolge ausgeführt wurden. Wie würde ich jedoch eine unbekannte Anzahl von Funktionen verketten?

Ich habe auch überlegt, benutzerdefinierte Ereignisse zu verwenden, aber ich habe das gleiche Problem.

44
dactyrafficle

Verwenden Sie HTMLAudioElement nicht für diese Art von Anwendung.

Die HTMLMediaElements sind von Natur aus asynchron und alles von der play() -Methode bis zur pause() -Methode und das Durchlaufen des offensichtlichen Ressourcenabrufs und der weniger offensichtlichen currentTime -Einstellung ist asynchron.

Dies bedeutet, dass für Elemente, die ein perfektes Timing benötigen (wie ein Morsecode-Leser), diese Elemente rein unzuverlässig sind.

Verwenden Sie stattdessen die Web-Audio-API und die Objekte AudioBufferSourceNode s, die Sie mit µs-Genauigkeit steuern können.

Rufen Sie zuerst alle Ihre Ressourcen als ArrayBuffers ab und generieren und spielen Sie dann bei Bedarf AudioBufferSourceNodes aus diesen ArrayBuffers.

Sie können diese synchron abspielen oder mit höherer Genauigkeit planen, als es Ihnen setTimeout bietet (AudioContext verwendet eine eigene Uhr).

Sie sind besorgt über die Auswirkungen auf den Speicher, wenn mehrere AudioBufferSourceNodes Ihre Samples abspielen? Sei nicht. Die Daten werden nur einmal im AudioBuffer gespeichert. AudioBufferSourceNodes sind nur Ansichten über diese Daten und nehmen keinen Platz ein.

// 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/[email protected]0c/build/promise-decode-audio-data.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Syncthetic/[email protected]/morsecode.js"></script>
<input type="text" id="inp" value="sos"><button id="btn" disabled>play</button>
44
Kaiido

Audios haben ein ended Ereignis, auf das Sie warten können, sodass Sie await ein Promise können, das aufgelöst wird, wenn dieses Ereignis ausgelöst wird:

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

Ich werde einen rekursiven Ansatz verwenden, der das Audio beendet Ereignis abhört. Jedes Mal, wenn die aktuelle Audiowiedergabe beendet wird, wird die Methode erneut aufgerufen, um die nächste abzuspielen.

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

Sie können die Prozedur, die playMorseArr() aufruft, mit dem Array und dem Startindex initialisieren:

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

Ein Testbeispiel (Verwenden der Dummy-Dateien mp3 Aus Kaiidos Antwort )

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

Obwohl sie für asynchrone Operationen verwendet werden, können sie auch für synchrone Aufgaben verwendet werden. Sie machen ein Versprechen für jede Funktion und verpacken sie in ein async function und rufen Sie sie dann einzeln mit await auf. Das Folgende ist die Dokumentation des async function Als benannte Funktion in der Demo ist die in der eigentlichen Demo eine Pfeilfunktion, aber so oder so sind sie eine in derselben:

 /**
  * 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

Demo

Hinweis: Wenn das Stack-Snippet nicht funktioniert, überprüfen Sie das 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