🎤

WebAudioAPIを利用して、PCマイクで録音した音声をバイナリデータ化してみる

2021/12/24に公開

WebAudioAPIとは

https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API
ざっくり言えば、「Web上でオーディオを処理したり合成したりするためのJavaScriptAPI」といった感じ。

バイナリデータとは

二進数(0,1)で表現されたデータであり、テキストデータ以外のデータ。
コンピュータが直接理解できる形で書いてあるため、人間が読んでもまず理解できない。
音声データ以外だと、画像ファイルや動画ファイル等がバイナリデータに含まれる。

処理の大まかな流れ

1.PCのマイクへ直接アクセス
2.録音した音声をバイナリデータ化
3.エンドポイントへ投げる(今回はPHP)

環境

$php -v
PHP 7.1.23 (cli) (built: Feb 22 2019 22:19:32) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies

録音処理部分

test

  //音声データのバッファをクリア
  audioData = [];

  //様々なブラウザでマイクへのアクセス権を取得
  navigator.mediaDevices = navigator.mediaDevices ||     navigator.webkitGetUserMedia;

  //audioのみtrue。Web Audioが問題なく使えるのであれば、第二引数で指定した関数を実行
  navigator.getUserMedia({
      audio: true,
      video: false
  }, successFunc, errorFunc);

  function successFunc(stream) {
    const audioContext = new AudioContext();
    sampleRate = audioContext.sampleRate;

    // ストリームを合成するNodeを作成
    const mediaStreamDestination =   audioContext.createMediaStreamDestination();

    // マイクのstreamをMediaStreamNodeに入力
    const audioSource = audioContext.createMediaStreamSource(stream);
    audioSource.connect(mediaStreamDestination);

    // 接続先のstreamをMediaStreamに入力
    for(let stream of remoteAudioStream){
        try{   
	audioContext.createMediaStreamSource(stream).connect(mediaStreamDestination);
          } catch(e){
              console.log(e);
          }
      }

    // マイクと接続先を合成したMediaStreamを取得
    const composedMediaStream = mediaStreamDestination.stream;
    // マイクと接続先を合成したMediaStreamSourceNodeを取得
    const composedAudioSource = audioContext.createMediaStreamSource(composedMediaStream);

    // 音声のサンプリングをするNodeを作成
    const audioProcessor = audioContext.createScriptProcessor(1024, 1, 1);
    // マイクと接続先を合成した音声をサンプリング
    composedAudioSource.connect(audioProcessor);

    audioProcessor.addEventListener('audioprocess', event => {
        audioData.push(event.inputBuffer.getChannelData(0).slice());
    });

    audioProcessor.connect(audioContext.destination);
}

録音した音声をバイナリデータ化

test
//音声をエクスポートした後のwavデータ格納用配列
    const waveArrayBuffer = [];
    //大きなデータを分けたうちの1つのデータ容量が25MB以下になるよう制御
    if (audioData.length > 250){
        const num = audioData.length/250;
        const count = Math.round(num);

        for (let i=0; i < count; i++){
            const sliceAudioData = audioData.slice(0,249);
            audioData.pop(0,249);
            const waveData = exportWave(sliceAudioData);
            waveArrayBuffer.push(waveData);
        }   
    }else{
        waveArrayBuffer.push(exportWave(audioData));
    }
   //PHPへPOST
    let oReq = new XMLHttpRequest();
    oReq.open("POST", '任意のパス', true);
    oReq.onload = function (oEvent) {
    // Uploaded.
    };

    //複数のデータをblob化するための配列
    const blob = [];
    //waveArrayBufferに入っている複数のデータを1つずつ配列に格納
    waveArrayBuffer.forEach(function(waveBuffer){
        blob.push(new Blob([waveBuffer], {type:'audio/wav'}));
    })

    var fd = new FormData();
    for (let i=0; i < blob.length; i++){
        fd.append('blob'+i,blob[i]);
    }
    // oReq.setRequestHeader('Content-Type','multipart/form-data; name="blob" boundary=\r\n');
    //配列ごとリクエスト送信
    oReq.send(fd);

    function exportWave(audioData) {
    // Float32Arrayの配列になっているので平坦化
    const audioWaveData = flattenFloat32Array(audioData);
    // WAVEファイルのバイナリ作成用のArrayBufferを用意
    const buffer = new ArrayBuffer(44 + audioWaveData.length * 2);

    // ヘッダと波形データを書き込みWAVEフォーマットのバイナリを作成
    const dataView = writeWavHeaderAndData(new DataView(buffer), audioWaveData, sampleRate);

    return buffer;
    }

    // Float32Arrayを平坦化する
    function flattenFloat32Array(matrix) {
        const arraySize = matrix.reduce((acc, arr) => acc + arr.length, 0);
        let resultArray = new Float32Array(arraySize);
        let count = 0;
        for(let i = 0; i < matrix.length; i++) {
            for(let j = 0; j < matrix[i].length; j++) {
            resultArray[count] = audioData[i][j];
            count++;
            }
        }
        return resultArray;
    }
    // ArrayBufferにstringをoffsetの位置から書き込む
    function writeStringForArrayBuffer(view, offset, str) {
        for(let i = 0; i < str.length; i++) {
            view.setUint8(offset + i, str.charCodeAt(i));
        }
    }

    // 波形データをDataViewを通して書き込む
    function floatTo16BitPCM(view, offset, audioWaveData) {
        for (let i = 0; i < audioWaveData.length; i++ , offset += 2) {
            let s = Math.max(-1, Math.min(1, audioWaveData[i]));
            view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
        }
    }

    // モノラルのWAVEヘッダを書き込む
    function writeWavHeaderAndData(view, audioWaveData, samplingRate) {
        // WAVEのヘッダを書き込み(詳しくはWAVEファイルのデータ構造を参照)
        writeStringForArrayBuffer(view, 0, 'RIFF'); // RIFF識別子
        view.setUint32(4, 36 + audioWaveData.length * 2, true); // チャンクサイズ(これ以降のファイルサイズ)
        writeStringForArrayBuffer(view, 8, 'WAVE'); // フォーマット
        writeStringForArrayBuffer(view, 12, 'fmt '); // fmt識別子
        view.setUint32(16, 16, true); // fmtチャンクのバイト数(第三引数trueはリトルエンディアン)
        view.setUint16(20, 1, true); // 音声フォーマット。1はリニアPCM
        view.setUint16(22, 1, true); // チャンネル数。1はモノラル。
        view.setUint32(24, samplingRate, true); // サンプリングレート
        view.setUint32(28, samplingRate * 2, true); // 1秒あたりのバイト数平均(サンプリングレート * ブロックサイズ)
        view.setUint16(32, 2, true); // ブロックサイズ。チャンネル数 * 1サンプルあたりのビット数 / 8で求める。モノラル16bitなら2。
        view.setUint16(34, 16, true); // 1サンプルに必要なビット数。16bitなら16。
        writeStringForArrayBuffer(view, 36, 'data'); // サブチャンク識別子
        view.setUint32(40, audioWaveData.length * 2, true); // 波形データのバイト数(波形データ1点につき16bitなのでデータの数 * 2となっている)

        // WAVEのデータを書き込み
        floatTo16BitPCM(view, 44, audioWaveData); // 波形データ

        return view;
    }

Discussion