web-dev-qa-db-ja.com

JavaScriptでオーディオファイルを同期再生するにはどうすればよいですか?

テキストをモールス信号オーディオに変換するプログラムに取り組んでいます。

sosと入力するとします。私のプログラムはこれを配列[1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]に変換します。ここで、s = dot dot dot(または1,1,1)、およびo = dash dash dash(または2,2,2)。この部分はとても簡単です。

次に、2つのサウンドファイルがあります。

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

私の目標は、dot.mp3を検出すると1を再生し、dash.mp3を検出すると2を再生し、0を検出すると一時停止する関数を用意することです。

次のような/種類/はうまくいくことがありますが、根本的に欠陥があり、どうすれば修正できるのかわかりません。

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

問題:

アレイをループしてサウンドファイルを再生することはできますが、タイミングは難しいです。 setTimeout()間隔を適切に設定しない場合、最後のオーディオファイルの再生が完了せず、250msが経過すると、配列の次の要素がスキップされます。したがって、dash.mp3dot.mp3よりも長くなります。タイミングが短すぎると、[dot dot dot pause dash dash pause dot dot dot]などの効果が聞こえる可能性があります。

欲しい効果

プログラムを次のようにしたい(疑似コード):

  1. ith配列要素を見てください
  2. 1または2の場合は、サウンドファイルの再生を開始するか、一時停止を作成します
  3. サウンドファイルを待つか、一時停止して終了する
  4. iをインクリメントし、ステップ1に戻ります

私が考えたことはありますが、実装方法がわかりません

だからピクルスはループを同期的に進行させたいということです。特定の順序で実行したい関数がいくつかある状況でプロミスを使用しましたが、不明な数の関数をチェーンするにはどうすればよいですか?

カスタムイベントの使用も検討しましたが、同じ問題があります。

44
dactyrafficle

この種のアプリケーションにはHTMLAudioElementを使用しないでください。

HTMLMediaElementsは本質的に非同期であり、play()メソッドからpause()までのすべてのものは、明らかなリソースのフェッチとそれほど明確ではないcurrentTime設定を経由して非同期です。

つまり、完璧なタイミングを必要とするアプリケーション(モールス符号リーダーなど)では、これらの要素はまったく信頼できません。

代わりに、Web Audio APIとその AudioBufferSourceNode sオブジェクトを使用します。これらのオブジェクトはµsの精度で制御できます。

最初にすべてのリソースをArrayBufferとしてフェッチし、次に必要に応じてこれらのArrayBufferからAudioBufferSourceNodesを生成して再生します。

これらを同期して再生したり、setTimeoutが提供するよりも高い精度でスケジュールしたりできます(AudioContextは独自のクロックを使用します)。

複数のAudioBufferSourceNodeがサンプルを再生することによるメモリへの影響が心配ですか?しないでください。データは、AudioBufferのメモリに1回だけ保存されます。 AudioBufferSourceNodesはこのデータの単なるビューであり、場所を取りません。

// 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には、リスニングできるendedイベントがあるため、そのイベントが発生したときに解決するawait a Promiseを実行できます。

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

私は、オーディオ 終了 イベントをリッスンする再帰的なアプローチを使用します。そのため、現在再生中のオーディオが停止するたびに、メソッドが再度呼び出されて次のオーディオが再生されます。

_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()を呼び出すプロシージャを初期化できます。

_playMorseArr([1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1], 0);
_

テスト例からのダミーの_mp3_ファイルを使用してカイドウの答え

_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

asyncawait

これらは非同期操作に使用されますが、同期タスクにも使用できます。関数ごとにPromiseを作成し、async functionでラップしてから、awaitで一度に1つずつ呼び出します。以下は、デモの名前付き関数としてのasync functionのドキュメントです。実際のデモの1つはアロー関数ですが、どちらかが同じものです。

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

<!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