ユーザーの音声を録音しています

現在、多くのブラウザには、ユーザーからの動画入力と音声入力にアクセスする機能が備わっています。ただし、ブラウザによっては、完全な動的インライン エクスペリエンスになる場合もあれば、ユーザーのデバイス上の別のアプリに委任される場合もあります。

シンプルに始めて、段階的に

最も簡単な方法は、お客様に事前に録画したファイルを提供するよう依頼することです。これを行うには、シンプルなファイル入力要素を作成し、音声ファイルのみを受け入れることを示す accept フィルタと、マイクから直接取得することを示す capture 属性を追加します。

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

この方法はすべてのプラットフォームで機能します。パソコンでは、ファイル システムからファイルをアップロードするようユーザーに求めます(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 を介して渡し、フィルタを適用する

音声データにアクセスする入力要素メソッドは、広く使用されていますが、最も魅力のないオプションです。マイクにアクセスして、ページ内で直接優れたエクスペリエンスを提供したいと考えています。

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

最新のブラウザはマイクに直接接続できるため、ウェブページと完全に統合されたエクスペリエンスを構築でき、ユーザーがブラウザを離れることはありません。

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

getUserMedia() という WebRTC 仕様の 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 です。このノードは、カスタム オーディオ処理のための低レベルの機能を提供します。実際の音声処理は、AudioWorkletProcessor の process() コールバック メソッドで行われます。この関数を呼び出して、入力とパラメータをフィードし、出力を取得します。

詳しくは、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 を呼び出した瞬間に、ブラウザはユーザーにサイトへのマイクへのアクセス許可を求めるメッセージを表示します。

ユーザーは、マシン上の強力なデバイスへのアクセスを求められることを嫌います。リクエストをブロックしたり、プロンプトが作成されたコンテキストを理解できない場合は無視したりすることがよくあります。マイクへのアクセスは、最初に必要になったときにのみリクエストすることをおすすめします。ユーザーがアクセスを許可すると、再度アクセスを求められることはありません。ただし、ユーザーがアクセスを拒否した場合、ユーザーに再度権限をリクエストすることはできません。

Permissions API を使用して、すでにアクセス権があるかどうかを確認する

getUserMedia API では、マイクへのアクセス権がすでに付与されているかどうかを把握できません。ここで問題が生じます。マイクへのアクセスを許可してもらうための優れた UI を提供するには、マイクへのアクセスをリクエストする必要があります。

この問題は、一部のブラウザでは Permission API を使用することで解決できます。navigator.permission API を使用すると、再度プロンプトを表示することなく、特定の API にアクセスする機能の状態をクエリできます。

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

  • 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 () {};
});

フィードバック