ユーザーの音声の録音

現在、多くのブラウザには、ユーザーによる動画および音声ファイルの入力を処理する機能が備わっています。ただしブラウザによっては、この機能が動的に組み込まれている場合や、ユーザーの端末上にある別のアプリに処理が委ねられる場合があります。

簡単なケースから始める

最も簡単な方法は、事前に録音したファイルをユーザーに依頼することです。そのためには、簡単なファイル入力要素を作成して、音声ファイルのみを受け入れる accept フィルタと、マイクで直接取得することを示す capture 属性を追加します。

<input type="file" accept="audio/*" capture />

この方法はすべてのプラットフォームで使用できます。PC の場合、ユーザーは、ファイル システムからファイルをアップロードするように求められます(capture 属性は無視されます)。iOS 上の Safari にこの方法を使用すると、マイクアプリが起動し、音声を録音してウェブページに送信できるようになります。Android の場合、ユーザーは音声をウェブページに送信する前に、音声の録音に使用するアプリを選択できます。

ユーザーが記録を終了してウェブサイトに戻ったら、なんらかの方法でファイルデータを取得する必要があります。ファイルにすばやくアクセスするには、onchange イベントを入力要素にアタッチして、イベント オブジェクトの files プロパティを読み取ります。

<input type="file" accept="audio/*" capture id="recorder" />
<audio id="player" controls></audio>
  <script>
    const recorder = document.getElementById('recorder');
    const player = document.getElementById('player');

    recorder.addEventListener('change', function (e) {
      const file = e.target.files[0];
      const url = URL.createObjectURL(file);
      // Do something with the audio file.
      player.src = url;
    });
  </script>
</audio>

ファイルにアクセスできるようになると、ファイルに対してあらゆる操作を行えます。たとえば、下記の設定が可能です。

  • <audio> 要素に直接アタッチして再生できるようにする
  • ユーザーのデバイスにダウンロードする
  • XMLHttpRequest にアタッチして、サーバーにアップロードする
  • Web Audio API を介してファイルを渡し、ファイルにフィルタを適用する

入力要素を使用して音声データにアクセスする方法は汎用的ですが、好ましい方法ではありません。理想的には、マイクにアクセスして、ページ内で適切なエクスペリエンスを直接提供する必要があります。

マイクにインタラクティブにアクセスする

最新のブラウザはマイクに直接アクセスできるため、ウェブページと完全に統合されたエクスペリエンスを実現できます。よって、ユーザーはブラウザから離れる必要がありません。

マイクへのアクセス権を取得する

WebRTC 仕様の getUserMedia() という API を使用すると、マイクに直接アクセスできます。getUserMedia() を使用すると、接続済みのマイクまたはカメラに対するアクセス権の付与を求めるメッセージがユーザーに表示されます。

アクセスが許可されると、API によって、カメラまたはマイクからのデータが含まれる Stream が返されます。このストリームは <audio> 要素や WebRTC ストリームにアタッチしたり、Web Audio の AudioContext にアタッチしたり、MediaRecorder API を使用して保存したりできます。

マイクからデータを取得するには、getUserMedia() API に渡される制約オブジェクトに audio: true を設定するだけです。

<audio id="player" controls></audio>
<script>
  const player = document.getElementById('player');

  const handleSuccess = function (stream) {
    if (window.URL) {
      player.srcObject = stream;
    } else {
      player.src = stream;
    }
  };

  navigator.mediaDevices
    .getUserMedia({audio: true, video: false})
    .then(handleSuccess);
</script>

特定のマイクを選択する場合は、まず使用可能なマイクを列挙することができます。

navigator.mediaDevices.enumerateDevices().then((devices) => {
  devices = devices.filter((d) => d.kind === 'audioinput');
});

その後、getUserMedia を呼び出すときに、使用する deviceId を渡すことができます。

navigator.mediaDevices.getUserMedia({
  audio: {
    deviceId: devices[0].deviceId,
  },
});

この機能だけでは、私たちができることは、音声データを取得して再生することだけです。

マイクの未加工データにアクセスする

マイクの未加工データにアクセスするには、getUserMedia() で作成されたストリームを取得し、Web Audio API を使用してそのデータを処理する必要があります。Web Audio API はシンプルな API であり、入力ソースを取得すると、それを音声データを処理(ゲインの調整など)できるノードに接続し、最終的にはユーザーが音声を聞くことができるようにスピーカーに接続します。

接続できるノードの 1 つは AudioWorkletNode です。このノードを使用すると、カスタム オーディオ処理の低レベル機能を利用できます。実際のオーディオ処理は、AudioWorkletProcessorprocess() コールバック メソッドで行われます。この関数を呼び出して、入力とパラメータをフィードし、出力を取得します。

詳しくは、Enter Audio Worklet をご覧ください。

<script>
  const handleSuccess = async function(stream) {
    const context = new AudioContext();
    const source = context.createMediaStreamSource(stream);

    await context.audioWorklet.addModule("processor.js");
    const worklet = new AudioWorkletNode(context, "worklet-processor");

    source.connect(worklet);
    worklet.connect(context.destination);
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess);
</script>
// processor.js
class WorkletProcessor extends AudioWorkletProcessor {
  process(inputs, outputs, parameters) {
    // Do something with the data, e.g. convert it to WAV
    console.log(inputs);
    return true;
  }
}

registerProcessor("worklet-processor", WorkletProcessor);

バッファに保持されたデータはマイクの未加工データであり、そのデータに対して多くの操作を行うことができます。次に例を示します。

  • データをサーバーに直接アップロードする
  • データをローカルで保存する
  • WAV などの専用のファイル形式に変換してから、サーバーまたはローカルに保存する

マイクのデータを保存する

マイクのデータを保存するには、MediaRecorder API を使用するのが最も簡単な方法です。

MediaRecorder API は、getUserMedia によって作成されたストリームを取得し、ストリーム上のデータを指定した宛先に段階的に保存します。

<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
  const downloadLink = document.getElementById('download');
  const stopButton = document.getElementById('stop');


  const handleSuccess = function(stream) {
    const options = {mimeType: 'audio/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function(e) {
      if (e.data.size > 0) recordedChunks.push(e.data);
    });

    mediaRecorder.addEventListener('stop', function() {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'acetest.wav';
    });

    stopButton.addEventListener('click', function() {
      mediaRecorder.stop();
    });

    mediaRecorder.start();
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess);
</script>

ここでは、あとで Blob に変換できる配列にデータを直接保存しています。その後、これを使用して、ウェブサーバーまたは直接ユーザーの端末のストレージにデータを保存します。

マイクを適切に使用するためにパーミッションを要求する

ユーザーが、サイトによるマイクへのアクセスを許可したことがない場合は、getUserMedia を呼び出すと、マイクにアクセスするためのパーミッションを付与するよう求める画面が表示されます。

ユーザーは、マシン上の高機能なデバイスへのアクセスを要求されるのを好まず、リクエストをブロックすることがよくあります。また、プロンプトが作成されたコンテキストを理解しないためにリクエストを無視することもあります。マイクへのアクセスは、最初に必要なときにのみリクエストすることをおすすめします。アクセス権が付与されると、ユーザーに再度プロンプトが表示されることはありません。ただし、ユーザーがアクセスを拒否した場合は、再度アクセスしてユーザーにパーミッションを要求できなくなります。

Permission API を使用してアクセス権の有無を確認する

getUserMedia API からの情報では、既にマイクへのアクセス権があるかどうかを確認できません。この場合、問題が発生します。ユーザーにマイクへのアクセス権を付与してもらう適切な UI を提供するには、マイクへのアクセスを要求する必要があります。

この問題は、一部のブラウザでは Permission API を使用すると解決できます。navigator.permission API を使用すると、プロンプトを再度表示する必要なく、特定の API にアクセスできるかどうかを照会できます。

ユーザーのマイクへのアクセス権があるかどうかを照会するには、{name: 'microphone'} をクエリメソッドに渡すと、以下のいずれかが返されます。

  • granted - ユーザーは以前にマイクへのアクセス権を付与しています。
  • prompt - ユーザーがアクセス権を許可していません。getUserMedia を呼び出すとメッセージが表示されます。
  • denied - システムまたはユーザーがマイクへのアクセスを明示的にブロックしているため、マイクにアクセスできません。

これで、ユーザーが行う必要があるアクションに対応するためにユーザー インターフェースを変更する必要があるかどうかをすばやく確認できます。

navigator.permissions.query({name: 'microphone'}).then(function (result) {
  if (result.state == 'granted') {
  } else if (result.state == 'prompt') {
  } else if (result.state == 'denied') {
  }
  result.onchange = function () {};
});

フィードバック