Web Audio API を使用して録音されるユーザの音声をリアルタイムにサーバに送信し、音声認識などの処理を逐次行うケースを想定しています。
今回作成したソースは GitHub に置いてあります。
動作確認環境
- macOS Mojave
- Node.js 11.6.0
- Express.js 4.16.4
- Google Chrome 73
1. 必要なパッケージのインストール
はじめに今回使用するパッケージのインストールを行います。
サーバとして Express を、サーバ・クライアント間の通信に socket.io を、最後に受信したバッファを WAVE ファイル形式で保存する為に wav-encoder を使用しています。
$ npm init
$ npm install --save express
$ npm install --save socket.io
$ npm install --save socket.io-client
$ npm install --save wav-encoder
2. サーバ側の実装
はじめに HTTP サーバを起動します。
なお、localhost 以外からサーバにアクセスする場合には HTTPS が必要となりますのでご注意ください。
const express = require('express')
const http = require('http')
const path = require('path')
const app = express()
app.use('/', express.static(path.join(__dirname, 'public')))
server = http.createServer(app).listen(3000, function() {
console.log('Example app listening on port 3000')
})
次に、WebSocket サーバを起動し、クライアントが接続したときの処理を記述します。今回はクライアントから受け付けるメッセージとして以下の3種類を定義します。
メッセージ | 説明 |
---|---|
start | 録音開始の合図。サンプリング周波数を受け取る。 |
send_pcm | 録音中のPCMデータを受け取るAPI。 |
stop | 録音停止の合図を受け取るAPI。受信した音声を public/wav の下に保存する。 |
今回の例では省略していますが、リアルタイムに音声認識などの処理を行いたい場合には send_pcm
の中で行うことになります。
// WebSocket サーバを起動
const socketio = require('socket.io')
const io = socketio.listen(server)
// クライアントが接続したときの処理
io.on('connection', (socket) => {
let sampleRate = 48000
let buffer = []
// 録音開始の合図を受け取ったときの処理
socket.on('start', (data) => {
sampleRate = data.sampleRate
console.log(`Sample Rate: ${sampleRate}`)
})
// PCM データを受信したときの処理
socket.on('send_pcm', (data) => {
// data: { "1": 11, "2": 29, "3": 33, ... }
const itr = data.values()
const buf = new Array(data.length)
for (var i = 0; i < buf.length; i++) {
buf[i] = itr.next().value
}
buffer = buffer.concat(buf)
})
// 録音停止の合図を受け取ったときの処理
socket.on('stop', (data, ack) => {
const f32array = toF32Array(buffer)
const filename = `public/wav/${String(Date.now())}.wav`
exportWAV(f32array, sampleRate, filename)
ack({ filename: filename })
})
})
クライアントから送られてくる PCM データは以下のオブジェクトの形式になっているため、配列に変換して buffer
に格納しています。
{ "1": 11, "2": 29, "3": 33, ... }
WAVE ファイルへの書き出しは WavEncoder を使用しています。WavEncoder へ渡すデータは Float32Array である必要があるため、下記の関数で変換を行っています。
// Convert byte array to Float32Array
const toF32Array = (buf) => {
const buffer = new ArrayBuffer(buf.length)
const view = new Uint8Array(buffer)
for (var i = 0; i < buf.length; i++) {
view[i] = buf[i]
}
return new Float32Array(buffer)
}
const WavEncoder = require('wav-encoder')
const fs = require('fs')
// data: Float32Array
// sampleRate: number
// filename: string
const exportWAV = (data, sampleRate, filename) => {
const audioData = {
sampleRate: sampleRate,
channelData: [data]
}
WavEncoder.encode(audioData).then((buffer) => {
fs.writeFile(filename, Buffer.from(buffer), (e) => {
if (e) {
console.log(e)
} else {
console.log(`Successfully saved ${filename}`)
}
})
})
}
3. クライアント側の実装
はじめにサンプリング周波数を start
を指定してサーバに送信します。
次に、getUserMedia
を使用してマイクにアクセスします。アクセス後は processor.onaudioprocess
にコールバック関数を指定することで、リアルタイムに音声を取得し、 send_pcm
を指定してサーバに送信しています。
const socket = io.connect()
let processor = null
let localstream = null
function startRecording() {
console.log('start recording')
context = new window.AudioContext()
socket.emit('start', { 'sampleRate': context.sampleRate })
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => {
localstream = stream
const input = this.context.createMediaStreamSource(stream)
processor = context.createScriptProcessor(4096, 1, 1)
input.connect(processor)
processor.connect(context.destination)
processor.onaudioprocess = (e) => {
const voice = e.inputBuffer.getChannelData(0)
socket.emit('send_pcm', voice.buffer)
}
}).catch((e) => {
// "DOMException: Rrequested device not found" will be caught if no mic is available
console.log(e)
})
}
録音終了時には getUserMedia
を停止させ、サーバに対して stop
を送信します
function stopRecording() {
console.log('stop recording')
processor.disconnect()
processor.onaudioprocess = null
processor = null
localstream.getTracks().forEach((track) => {
track.stop()
})
socket.emit('stop', '', (res) => {
console.log(`Audio data is saved as ${res.filename}`)
})
}
最後にテスト用として録音の開始・停止ボタンのみの画面を作成しておきます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="/socket.io/socket.io.js" ></script>
<script src="./javascripts/client.js" ></script>
</head>
<body>
<button onClick="startRecording()">start</button>
<button onClick="stopRecording()">stop</button>
</body>
</html>
4. 動作確認
以下のコマンドでサーバを起動してブラウザから localhost:3000
にアクセスして、"start" ボタンをクリックしてマイクへ音声を入力後 "stop" ボタンをクリックすると、public/wav
フォルダ内に WAVE ファイルが保存されます。
$ node app.js