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

#概要
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)
  }
```
#おわりに
ご指摘等ありましたら宜しくお願い致します！