IBM Bluemix/Watson Speech To Text/Node.jsでApplication開発(2)

[前回](http://qiita.com/yoshiono/items/fe5bc375f9f154e101fa)からの続きで、Javascripソースの記述について説明します。

**2. wavファイル生成Javascripソースの記述**
app.jsを作成します。ExpressやEJSについては、偉大な先達の方々の[知見1](http://libro.tuyano.com/index2?id=1115003)や、[公式サイト](http://expressjs.com/ja/starter/generator.html)、BluemixにDeployするためのportの設定については[手順3](http://www.ibm.com/developerworks/jp/cloud/library/j_cl-bluemix-nodejs-app/)の記述を参考にしています。なお、'STTで音声認識Javascriptソースが動作するURLを記述'には、後述の音声認識処理サーバーのURLを設定します。
ところで、コメント部分は、[クロスオリジン HTTP リクエスト](https://developer.mozilla.org/ja/docs/HTTP_access_control)の制限解除を試みた設定ですが、有効化できていません。不要ですが、敢えて、残しています。また、CDNとしてajaxを使うためのタグがあります。結果としてajaxは使っていないので不要ですが、これまた、敢えて残しています。

```javascript
:app.js
var express = require('express');
var ejs = require("ejs");
  
var app = express();

//  app.use(function(req, res, next) {
//    res.setHeader("Access-Control-Allow-Origin", "https://stream.watsonplatform.net");
//    res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT");
//    res.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type");
//    res.setHeader("Access-Control-Allow-Credentials", true);
//    return next();
//  });
  
app.engine('ejs',ejs.renderFile);
  
app.get("/", function(req, res){
    res.render('test.ejs', 
        {title: 'Test Page' , 
            content: 'this is test.'});
});
  
//var server = app.listen(3000, function(){
//    console.log('Server is running!');
//})

var server = app.listen(process.env.PORT || 3000, function() {
  console.log('Listening on port %d', server.address().port);
});
```
次に、test.ejsを作成します。EJSの仕組みについては、
偉大な先達の方々の[知見1](http://libro.tuyano.com/index2?id=1115003)を、WebRTC、wavファイルの生成については、[知見2](http://www.cyokodog.net/blog/media-capture-and-streams-web-audio-api/)、[知見3](http://qiita.com/HirokiTanaka/items/56f80844f9a32020ee3b)を参考にしています(参考というより、そのモノです)。ソース中の、'STTで音声認識Javascriptソースが動作するURLを記述'の部分に、後述のBluemixでDeployにおける、cf push mysttで得られたurlsの値を設定します。

```js:test.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="content-type"
        content="text/html; charset=UTF-8">
    <title><%=title %></title>
    <style>
    body { font-size:12pt; color:#000066; }
    h1 { font-size:18pt; background-color:#DDDDFF; }
    pre { background-color:#EEEEEE; }
    </style>
    </head>
<body>
    <header>
        <h1><%=title %></h1>
    </header>
    <article>
        <%-content %>
    </article>
        <button id="StartRecording">StartRecording</button>
        <button id="StopRecording">StopRecording</button>
        <button id="StartPlaying">StartPlaying</button>
        <button id="GenerateWAV">GenerateWAV</button>
        <button id="DownloadWAV">DownloadWAV</button>
        <button id="Upload">Upload</button>

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
        <script>
    window.Recorder = function(audioContext, bufferSize){
        var o = this;
        o.audioContext = audioContext;
        o.bufferSize = bufferSize || 4096;
    }

    Recorder.prototype = {
        audioContext : '',
        bufferSize : '',
        audioBufferArray : [],
        stream : '',
        recording : function(stream){
            var o = this;
            o.stream = stream;
            var mediaStreamSource =
                o.audioContext.createMediaStreamSource(stream);
            var scriptProcessor =
                o.audioContext.createScriptProcessor(o.bufferSize, 1, 1);
            mediaStreamSource.connect(scriptProcessor);
            o.audioBufferArray = [];
            scriptProcessor.onaudioprocess = function(event){
                var channel = event.inputBuffer.getChannelData(0);
                var buffer = new Float32Array(o.bufferSize);
                for (var i = 0; i < o.bufferSize; i++) {
                    buffer[i] = channel[i];
                }
                o.audioBufferArray.push(buffer);
            }
            //この接続でonaudioprocessが起動
            scriptProcessor.connect(o.audioContext.destination);
            o.scriptProcessor = scriptProcessor;
        },
        recStart : function(){
            var o = this;
            if(o.stream){
                o.recording(o.stream);
            }
            else{
                navigator.getUserMedia(
                    {video: false, audio: true},
                    function(stream){o.recording(stream)},
                    function(err){
                        console.log(err.name ? err.name : err);
                    }
                );
            }
        },
        recStop : function(){
            var o = this;
            o.scriptProcessor.disconnect();
            if(o.stream){
                o.stream.stop();
                o.stream = null;
            }
        },
        getAudioBufferArray : function(){
            var o = this;
            return o.audioBufferArray
        },
        getAudioBuffer : function(){
            var o = this;
            var buffer = o.audioContext.createBuffer(
                1,
                o.audioBufferArray.length * o.bufferSize,
                o.audioContext.sampleRate
            );
            var channel = buffer.getChannelData(0);
            for (var i = 0; i < o.audioBufferArray.length; i++) {
                for (var j = 0; j < o.bufferSize; j++) {
                    channel[i * o.bufferSize + j] = o.audioBufferArray[i][j];
                }
            }
            return buffer;
        }
    }

    window.exportWAV = function(audioData, sampleRate) {
        var encodeWAV = function(samples, sampleRate) {
            var buffer = new ArrayBuffer(44 + samples.length * 2);
            var view = new DataView(buffer);
            var writeString = function(view, offset, string) {
                for (var i = 0; i < string.length; i++){
                    view.setUint8(offset + i, string.charCodeAt(i));
                }
            };
            var floatTo16BitPCM = function(output, offset, input) {
                for (var i = 0; i < input.length; i++, offset += 2){
                    var s = Math.max(-1, Math.min(1, input[i]));
                    output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                }
            };
            writeString(view, 0, 'RIFF');  // RIFFヘッダ
            view.setUint32(4, 32 + samples.length * 2, true); // これ以降のファイルサイズ
            writeString(view, 8, 'WAVE'); // WAVEヘッダ
            writeString(view, 12, 'fmt '); // fmtチャンク
            view.setUint32(16, 16, true); // fmtチャンクのバイト数
            view.setUint16(20, 1, true); // フォーマットID
            view.setUint16(22, 1, true); // チャンネル数
            view.setUint32(24, sampleRate, true); // サンプリングレート
            view.setUint32(28, sampleRate * 2, true); // データ速度
            view.setUint16(32, 2, true); // ブロックサイズ
            view.setUint16(34, 16, true); // サンプルあたりのビット数
            writeString(view, 36, 'data'); // dataチャンク
            view.setUint32(40, samples.length * 2, true); // 波形データのバイト数
            floatTo16BitPCM(view, 44, samples); // 波形データ
            return view;
        };
        var mergeBuffers = function(audioData) {
            var sampleLength = 0;
            for (var i = 0; i < audioData.length; i++) {
              sampleLength += audioData[i].length;
            }
            var samples = new Float32Array(sampleLength);
            var sampleIdx = 0;
            for (var i = 0; i < audioData.length; i++) {
              for (var j = 0; j < audioData[i].length; j++) {
                samples[sampleIdx] = audioData[i][j];
                sampleIdx++;
              }
            }
            return samples;
        };
        var dataview = encodeWAV(mergeBuffers(audioData), sampleRate);
        var audioBlob = new Blob([dataview], { type: 'audio/wav' });
        return audioBlob;
    };

    navigator.getUserMedia = 
        navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.msGetUserMedia;
    window.URL = 
        window.URL || 
        window.webkitURL || 
        window.mozURL || 
        window.msURL;
    window.AudioContext = 
        window.AudioContext||
        window.webkitAudioContext;


//以下、アプリのためのロジック

var audioContext = new AudioContext();
var recorder = new Recorder(audioContext);
var localurl = null;

function startRecording() {
recorder.recStart(); // 録音開始
};

function stopRecording() {
recorder.recStop(); // 録音停止
recorder.getAudioBufferArray(); //音声配列データの取得
recorder.getAudioBuffer(); //AudioBuffer の取得
};

function startPlaying() {
    var src = audioContext.createBufferSource();
    src.buffer = recorder.getAudioBuffer();
    src.connect(audioContext.destination);
    src.start()
};

function generateWAV() {
    var blob = exportWAV(recorder.getAudioBufferArray(), audioContext.sampleRate)
    localurl = URL.createObjectURL(blob);
    console.log(localurl);
};

//querySelectorでは、#をつけることで、idがbtnStartMonitoringの要素を取得している(つまり、Startボタン)
document.querySelector("#StartRecording").onclick = startRecording; // Wire up start button
document.querySelector("#StopRecording").onclick = stopRecording; // Wire up stop button 
document.querySelector("#StartPlaying").onclick = startPlaying; //
document.querySelector("#GenerateWAV").onclick = generateWAV; //
document.querySelector("#DownloadWAV").onclick = function() {
    console.log(localurl);
    return location.href = localurl;
};

document.querySelector("#Upload").onclick = function() {
    console.log('Upload');
    return location.href = 'STTで音声認識Javascriptソースが動作するURLを記述';
};

        </script>

</body>
</html>
```
[3. STTで音声認識Javascriptソースの記述](http://qiita.com/yoshiono/items/536332abd60ba3bde3f9)に続きます。