LoginSignup
5
7

More than 3 years have passed since last update.

【WebAudioAPI】録音した音声をバイナリデータ化、PHPへ受け渡し

Last updated at Posted at 2020-02-05

概要

Node.js上で、IBMのWatsonによって人が話した音声データを自動で文字起こしするスクリプトを作成しました。
その中で、結構苦労した
PCのマイクに直接アクセス→録音した音声データをバイナリデータ化、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

録音部分

hogehoge.js
// 音声データのバッファをクリアする
    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);
    }

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

hogehoge.js
//音声をエクスポートした後の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
    var 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;
    }

リクエスト受け取り部分(超絶一部抜粋)

hogehoge.php
//リクエスト受け取り
$req = $_FILES
var_dump($req);

//出力結果
array(2) {
  ["blob0"]=>
  array(5) {
    ["name"]=>
    string(4) "blob"
    ["type"]=>
    string(9) "audio/wav"
    ["tmp_name"]=>
    string(14) "/tmp/ランダム文字列"
    ["error"]=>
    int(0)
    ["size"]=>
    int(509996)
  }

おわりに

ご指摘等ありましたら宜しくお願い致します!

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7