ブラウザで取得した音声データを、サーバーでwav形式で保存する

PCに繋いだマイクから簡単にサーバーに音声を送れないか、いろいろと調べた結果のメモです。

社内向けのちょっとしたツール用なので、「とりあえずGoogle Chromeの最新版で動けばいいや」という仕様です。

ほとんど自分用の覚書ですし、私はJavaScriptの知識はほとんどありません。Pythonはサーバーサイドやデータ分析用途でよく使っています。

もしかすると音声通信用の良いJSのライブラリがあり、以下に述べるような作業が一発で終わってしまうかもしれません。もしご存じでしたらコメントで教えてください！

# ブラウザで音声を取得し、websocketで送信する

これは、GoogleのWEB開発者向け記事の『[ユーザーから音声データを取得する](https://developers.google.com/web/fundamentals/native-hardware/recording-audio/?hl=ja)』が参考になりました。

```html
<script>  
  var handleSuccess = function(stream) {
    var context = new AudioContext();
    var input = context.createMediaStreamSource(stream)
    var processor = context.createScriptProcessor(1024, 1, 1);

    // WebSocketのコネクション
    var connection = new WebSocket('wss://hogehoge.com:8000/websocket');

    input.connect(processor);
    processor.connect(context.destination);

    processor.onaudioprocess = function(e) {
      var voice = e.inputBuffer.getChannelData(0);
      connection.send(voice.buffer); // websocketで送る
    };
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess)
</script>
```

## 1. サーバーに音声を送り続ける方法について

音声をサーバーに送る方法について、以下の3つの方法があるようです。

1. http/httpsでポーリングする
2. WebSocket通信(ws/wss)で断続的に送る
3. WebRTCを利用する

ここでは、Qiitaの『[サーバからクライアントに送信する技術 - WebSocketを中心に](http://qiita.com/yuba/items/00fc1892b296fb7b8de9)』という記事や、『[WebSocket / WebRTCの技術紹介](https://www.slideshare.net/mawarimichi/websocketwebrtc)』が参考になりました。

「http通信で高頻度で送るのもアレだし、双方向の音声通信が必要でもないのにWebRTCを使うのも重そうだね」ということで、ひとまず音声のバッファをwebsocketで送信し続けることにしました。WebSocketではバイナリか文字列を送受信できるようです。

## 2. AudioContext.createScriptProcessorで一度に送る音声のサイズを決める

詳しくは[こちらのドキュメント](https://developer.mozilla.org/ja/docs/Web/API/AudioContext/createScriptProcessor)を読んでください。

> サンプルフレームを単位としたバッファのサイズです。指定する場合は、次のいずれかの値でなくてはなりません: 256, 512, 1024, 2048, 4096, 8192, 16384 。指定されない場合、もしくは 0 が指定された場合、環境における最適な値が設定されます。この値はノードが生存する限り同じ値が利用され、その値は 2 の冪上です。
>
> この値は audioprocess イベントの発生頻度と、イベントごとに渡されるサンプルフレームの大きさを決めます。小さい値を指定すると低遅延となり、大きな値を指定すると音声の破損やグリッチを避けられます。この値は自分で決めず、実装に決めさせることが遅延と品質の面から推奨されます。

この「実装に決めさせる（原文ではallow the implementation to pick a good buffer size）」という言葉の正確な意味がわからないのですが、0を設定しろということでしょうか？

[こちらのスタックオーバーフロー(2013年)](https://stackoverflow.com/questions/19986980/does-google-chrome-web-audio-api-implementation-support-scriptprocessornode-buff)でも議論されています。

## 3. AudioBuffer.getChannelDataで音声を取得する

こちらも[ドキュメント](https://developer.mozilla.org/ja/docs/Web/API/AudioBuffer/getChannelData)を読んでください。ここではモノラル音声を扱っていて、ステレオ音声が必要な場合は、工夫する必要があるみたいです。

bufferを呼び出すと、Float32Array型、つまり-1~+1までの範囲の実数で音声データが返ってきます。WAVファイルでは（いくつか形式があるようですが）16ビット符号付き整数、つまり-32768~32767の値で表現されることに注意する必要があります。


# サーバーでWAVファイルとして保存する

こちらは、私が使い慣れているPython3.6を利用しました。

Pythonの非同期処理が得意な軽量WEBフレームワークであるTornadoを利用しました。

```python
import tornado.ioloop
import tornado.web
import tornado.websocket

import wave
import numpy as np


SAMPLE_SIZE = 2
SAMPLE_RATE = 48000
PATH = '/path/to/output.wav'


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        self.voice = []
        print("opened")

    def on_message(self, message):
        self.voice.append(np.frombuffer(message, dtype='float32'))

    def on_close(self):
        v = np.array(self.voice)
        v.flatten()

        # バイナリに16ビットの整数に変換して保存
        arr = (v * 32767).astype(np.int16)
        with wave.open(PATH, 'wb') as wf:
            wf.setnchannels(1)
            wf.setsampwidth(SAMPLE_SIZE)
            wf.setframerate(SAMPLE_RATE)
            wf.writeframes(arr.tobytes('C'))
        
        self.voice.clear()
        print("closed")


app = tornado.web.Application([
    (r"/websocket", WebSocketHandler)
])

if __name__ == "__main__":
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()
```

## 1. Tornadoについて

Tornadoを使ったWebSocketサーバーのドキュメントは[こちらです](http://www.tornadoweb.org/en/stable/websocket.html)。

他にもFlaskやBottle、Djangoなどのフレームワークが有名ですが、Tornadoはnode.js的な非同期処理が得意だということで採用しました。また、他のフレームワークに比べて参考にできるコードが多そうだったということもあります。

## 2. バイナリの読み込みにnumpyが楽

Pythonの数値計算用ライブラリのnumpyを使えば、渡されたデータを簡単に読み込み、numpyの配列として読み込むことができます。

詳しくは[numpy.frombufferのリファレンス](https://docs.scipy.org/doc/numpy/reference/generated/numpy.frombuffer.html)を読みましょう。

## 3. wavファイルについて

WAVファイルでは（いくつか形式があるようですが）16ビット符号付き整数、つまり-32768~32767の値で表現されることに注意する必要があります。

また、サンプルレートの値（48000）はJavaScript側の実装によるので、[AudioContext.sampleRate](https://developer.mozilla.org/ja/docs/Web/API/AudioContext/sampleRate)で確認してください。

WAVのデータ構造の仕様が分からずに困っていたのですが、『[C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理](https://www.amazon.co.jp/dp/4274206505)』の第一章の解説を読んで勉強しました。

まあ、結局Pythonの標準ライブラリの[wave](https://docs.python.jp/3/library/wave.html)を使えばなんとかできたのですが。

# まとめ

以上です。

ここから、いろいろなクラウドサービスの音声APIに送ったり、Pythonで音声処理をしたり、いろいろできると思います。また、認証も何も作っていないので、そこもちゃんとしなければいけません。

WebSocketを使ったのも、サーバーからクライアント（ブラウザ）へのプッシュができて、音声解析APIや音声処理の結果をリアルタイムで返せると面白そうだからという理由もあります。