ブラウザでgetUserMediaしたストリームからflacファイルを作成してみる

これは2017年[WebRTCアドベントカレンダー](https://qiita.com/advent-calendar/2017/webrtc)の23日目の記事です。

# **はじめに**


最近、話題のスマートスピーカー[Google HomeのCM](https://www.youtube.com/watch?v=Dj4Oilf3fjs)ようなことブラウザでもやりたい。

ということで

いろいろ調べてみると最もお手軽なのは[Web_Speech_API](https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API)したが、ブラウザ互換性を見ると
Safari未サポートということなのでcloud speech API用の音声ファイルを作ってみます。

ちなみに[Google Cloud Speech API](https://cloud.google.com/speech/?hl=ja)は[LINEAR16 または MULAW でエンコードされたwavファイルも対応](https://cloud.google.com/speech/docs/basics?hl=ja#audio-encodings)
しているので[ブラウザでマイク音声からwavファイルを生成する手引 1](https://qiita.com/gcnishino/items/ad141b6a3b4ba2f4197b)
こちらで十分なのですが

今回は違う形式の音声ファイル(flac)をブラウザで作成したいので
ライブラリないかなと探していた所、いいのありました！

# **ライブラリ**
Flacエンコード用
https://github.com/mmig/libflac.js
 MIT License

Blob保存
https://github.com/eligrey/FileSaver.js
 MIT License

*WebWorkerを使ったDemoも用意されてます*
https://mmig.github.io/speech-to-flac/

# **実装方法**

```html
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>flac recorder</title>
</head>
<body>
    <input type="button" id="start-button" onclick="startRecording();" value="Rec"></input>
    <input type="button" id="stop-button" onclick="stopRecording();" value="Stop"></input>
    <script type="text/javascript" src="libflac3-1.3.2.min.js" /></script>
    <script type="text/javascript" src="FileSaver.min.js" /></script>
    <script type="text/javascript">
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    var encoder = null;
    var recording = false;
    var localStream = null;
    var audio_context = null;
    var source = null;
    var node = null;

    function startRecording() {
        //worker開始
        encoder = new Worker('encoder.js');
        encoder.onmessage = function(evt) {
            if (evt.data.cmd == 'end') {
                //flac保存処理
                saveAs(evt.data.buf, 'output.flac');
                //worker破棄
                encoder.terminate();
                encoder = null;
            }
        };

        navigator.getUserMedia({ video: false, audio: true },
            function(stream) { // for success case
                localStream = stream;
                gotStream(localStream);
            },
            function(err) { // for error case
                console.log(err);
            }
        );
    }


    function gotStream(stream) {
        console.log('start recording');
        recording = true;

        var audio_context;
        if (typeof webkitAudioContext !== 'undefined') {
            audio_context = new webkitAudioContext;
        } else if (typeof AudioContext !== 'undefined') {
            audio_context = new AudioContext;
        }
        source = audio_context.createMediaStreamSource(stream);

        if (source.context.createJavaScriptNode) {
            node = source.context.createJavaScriptNode(4096, 1, 1);

        } else if (source.context.createScriptProcessor) {
            node = source.context.createScriptProcessor(4096, 1, 1);
        }

        var samplerate = audio_context.sampleRate; //44100 または 48000
        //flac初期処理
        encoder.postMessage({ cmd: 'init', config: { samplerate: samplerate, bps: 16, channels: 1, compression: 5 } });

        node.onaudioprocess = function(evt) {
            if (!recording) {
                return;
            }

            var channel = evt.inputBuffer.getChannelData(0);
        　　　//flacエンコード
            encoder.postMessage({ cmd: 'encode', buf: channel });
        };

        source.connect(node);
        node.connect(audio_context.destination);
    }

    function stopRecording() {
        if (!recording) {
            return;
        }
        console.log('stop recording');
        var tracks = localStream.getAudioTracks()
        for (var i = tracks.length - 1; i >= 0; --i) {
            tracks[i].stop();
        }
        recording = false;
        //worker終了
        encoder.postMessage({ cmd: 'finish' });

        source.disconnect();
        node.disconnect();
        source = node = null;
    };
    </script>
</body>

</html>
```






# **おわりに**

そのうちMediaRecorderのmimeTypeにflacが追加されるのを期待しますｗ！