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