サーバ側プログラム
冒頭で解説した通り、今回はmbedからの音声データをいったんBluemix上に配置したアプリケーション(プロクシ)で受け取り、それをWSTにHTTPSで転送します。プロクシは、Play Frameworkで実装しました。実行にはJava 8が必要なので適宜インストールしておいてください。Oracle版は、米Oracle社のWebサイトからダウンロード可能です。
インストールしたら、念のため端末からjava -versionを実行して、Java 8がインストールされていることを確認しておきます。
$ java -version java version "1.8.0_91" Java(TM) SE Runtime Environment (build 1.8.0_91-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
今回はMacでの手順を示します(Linuxでも同じ手順で実行できるはずです。シェルスクリプトを使用しているため、Windowsの場合はそのままでは動作しません。仮想マシンを使うなどして、Linuxの環境を用意してください)。プロクシのプログラムは、GitHub上に置いてあるので以下で取得してください(Gitのインストールについては解説を省略します)。
$ git clone https://github.com/ruimo/cz-mbed7.git
なお、これ以降の手順は、依存ライブラリのダウンロードや、Dockerイメージの取得などが行われるため、多量の通信が発生します。従量制のモバイル通信などで実行すると、想定外の通信量が発生する場合があるのでご注意ください。
cz-mbed7ディレクトリに移り、次にwatson-env.conf.sampleを、watson-env.confにコピーして、中身を編集します。なお、編集した後のwatson-env.confは機密情報なので、他の人の目に触れないように注意してください。
VCAP_SERVICES_SPEECH_TO_TEXT_0_CREDENTIALS_USERNAME='Watson speech to text username' VCAP_SERVICES_SPEECH_TO_TEXT_0_CREDENTIALS_PASSWORD='Watson speech to text password' VCAP_SERVICES_SPEECH_TO_TEXT_0_CREDENTIALS_URL='https://stream.watsonplatform.net/speech-to-text/api' APP_SECRET='Playframework app secret' DOCKER_NS='Bluemix docker name space'
最初の2行は、WSTの認証情報です。図10の内容をシングル・クォートの中に指定します。APP_SECRETや、DOCKER_NSはまだ設定しなくて構いません。ファイルを保存したらlocal-run.shを実行し、プロンプトにrunと入力して実行します。
[watsonproxy] $ run --- (Running the application, auto-reloading is enabled) --- [info] p.c.s.NettyServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...)
このように表示されてmbedからのリクエスト待ちとなります。
mbed側プログラム
概要
今回は通信にhttpを使うので、ライブラリとしてEthernetInterface、HTTPClient、mbed-rtosを追加します。ライブラリの追加の方法については、連載第3回、第4回で解説しましたので、そちらを参照してください。プログラムは、main.cpp、WavPostData.cpp、WavPostData.hの3つです。今回のmbedのプログラムは少々長いので、図11に処理の概略フローチャートを、リスト1にmain.cpp全体を示します。
#include "mbed.h"
#include <stdio.h>
#include <stdlib.h>
#include "EthernetInterface.h"
#include "HTTPClient.h"
#include "WavPostData.h"
static void mainLoop(void);
static Ticker timer;
static AnalogIn ain(p18);
static DigitalOut led1(LED1);
static DigitalOut led2(LED2);
static DigitalOut led3(LED3);
static DigitalOut led4(LED4);
static DigitalIn startBtn(p14);
static PwmOut rled(p23);
static PwmOut gled(p24);
static PwmOut bled(p25);
static EthernetInterface eth;
static HTTPClient httpClient;
static char responseText[512];
static HTTPText response(responseText, sizeof responseText);
typedef enum {
OFFSET, RECORDING, FINISHED
} State;
static volatile State state = OFFSET; // (5)
static int idx = 0;
static int32_t offset = 0;
#pragma pack(1)
typedef struct { // (13)
int wav0 : 12;
int wav1 : 12;
} WavPack;
static WavPack wavData[6000];
#pragma pack()
const int BUF_SIZE = sizeof(wavData) / sizeof(wavData[0]) * 2;
static void onTick(void) {
int16_t a = (int16_t)(ain * 2047); // (9)
switch (state) {
case OFFSET: // (10)
if (idx < 10) {
offset += a;
++idx;
}
else { // (11)
state = RECORDING;
offset /= 10;
idx = 0;
led1 = 1;
}
break;
case RECORDING: // (12)
a = a - offset; // (14)
if (idx >= BUF_SIZE) {
led2 = 1;
state = FINISHED;
}
else {
WavPack *p = &wavData[idx / 2];
if (idx++ % 2 == 0) p->wav0 = a;
else p->wav1 = a;
}
break;
default: // (15)
break;
}
}
int main() { // (1)
rled = 0.5; gled = 0.5; bled = 1;
eth.init();
eth.connect();
rled = 1; gled = 0.5; bled = 1;
while (1)
mainLoop();
}
static void mainLoop(void) {
state = OFFSET;
idx = 0;
offset = 0;
while (startBtn == 0) { // (2)
wait_us(100 * 1000);
}
timer.attach_us(onTick, 125); // (3)
while (state != FINISHED) { // (4)
wait(1);
}
timer.detach(); // (6)
led3 = 1;
WavPostData request(&wavData, sizeof wavData); // (7)
responseText[0] = '\0';
// int responseCode = httpClient.post("http://134.168.4.206/", request, &response);
int responseCode = httpClient.post("http://192.168.0.10:9000/", request, &response);
led1 = 0;
led2 = 0;
led3 = 0;
led4 = strncmp(responseText, "ON", 2) == 0 ? 1 : 0; // (8)
}
- (1)処理は最初にイーサネット・ポートの初期化を行った後、main loop処理を無限に繰り返すようになっています。
- (2)main loop処理では、最初にjoy stickボタンの押下を待ち(joy stickは上下左右に動かせるだけでなく、押し込めるようになっています)、押されたタイミングで録音を開始します(押したタイミングで録音を開始するだけなので、すぐに離して構いません。録音終了はバッファを使い切った段階で自動的に停止します)。
- (3)録音処理はmbedのタイマ機能を使用します。今回はサンプリング周波数が8kHzなので、その逆数を取ると、1/8000=0.000125=125×10^-6となり、125マイクロ秒に1回、アナログ・ポートのデータを読み出せば良いことが分かります。mbedのタイマ・ライブラリに125マイクロ秒に1度処理を呼び出してもらうように依頼しておけば(このときに関数ポインタで処理を指定します)、指定した時間経過ごとに指定した処理(今回はonTick関数)が呼び出されるようになります。
- (4)実際の録音処理はタイマ処理側で行われるので、main loop側は状態変数(state)がFINISHEDに変化するまで待ちます(state変数は、onTickの中で変更します)。
- (5)state変数はタイマ処理とmain loopの両方で参照するため、volatile宣言が必要な点に注意してください。
- (6)音声取り込みが終わればタイマ処理を削除します。
- (7)HttpClientを用いて、音声データをプロクシにuploadします。
- (8)プロクシから返ってきたコマンドに従ってLED4をon/offします。
それでは心臓部のonTick関数を見てみましょう。回路の説明で述べた通り、mbedのアナログ入力の制約から、音声データは0V以上の範囲に入るように「かさ上げ」されています。ここからwavのような音声ファイルを生成するには、0Vを中心に振幅する信号に復元しなければなりません。ボタンを押した直後は無音状態であることを仮定し、最初の1250マイクロ秒の間に得られた信号の値の平均がゼロ点だと仮定して、オフセット値とします。以降の信号は、このオフセットの値を測定値から減算することで、元の信号を復元します。
以上の処理を簡潔に書くため、音声取り込みは簡単なステート・マシーンになっています。最初はOFFSETという状態で始まり、1250マイクロ秒経過する(タイマからの呼び出しが10回に逹っする)と、RECORDINGという状態に移ります。録音用のバッファがフルになると状態はFINISHEDという状態に変わります。main loop()関数は、上の(4)で見た通り、このFINISHEDへの変化を合図に音声のプロクシへのアップロードを行います(図12)。
- (9)mbedのD/Aコンバータは12bitの精度しかないので、アナログ入力は12bit(0-2047)で扱います。
- (10)状態がOFFSETの場合は、タイマ10回分の値をoffset変数に加算していきます。
- (11)10回分経過したら、offset変数の値を10で除算して平均値を求め、これをoffsetとして用います。この時LED1を点灯して、音声の録音が始まったことが分かるようにしています。state変数をRECORDINGに変更します。
- (12)状態がRECORDINGの場合は、バッファがフルになるまで音声データを記録します。
- (13)12bitのデータを効率良く記録するためビットフィールドを定義しています。12bitのデータ2つを含んだ構造体にpackプラグマを指定して、バイト境界にキッチリと詰めるようにすることで、3バイトに収めます。
- (14)ビットフィールドに定義したバッファに音声データを格納しています。測定データからは、上で計算しておいたoffsetを減算することで、0Vを中心に振幅するデータを復元します。バッファがフルになったら状態をFINISHEDに変更します。
- (15)FINISHED状態になると、それ以降は何も行いません。
最後にWavPostData.cpp、WavPostData.hについて簡単に解説します。mbedのHTTPClientはデフォルトではテキストを扱うための仕組みしかないため、今回のようにバイナリデータを送信したい場合には、自分でIHTTPDataOutの継承クラスを用意する必要があります(リスト2-1、リスト2-2)。
#ifndef WAV_POST_DATA_H_
#define WAV_POST_DATA_H_
#include <IHTTPData.h>
class WavPostData: public IHTTPDataOut {
public:
WavPostData(const void *p, size_t size);
protected:
virtual void readReset();
virtual int read(char* buf, size_t len, size_t* pReadLen);
virtual int getDataType(char* type, size_t maxTypeLen);
virtual bool getIsChunked();
virtual size_t getDataLen();
virtual bool getHeader(char *header, size_t maxHeaderLen);
private:
const char * const pData;
const size_t dataSize;
size_t pos;
};
#endif /* WAV_POST_DATA_H_ */
リスト2-2 バイナリデータ送信用データ構造(WavPostData.cpp)
#include "WavPostData.h"
#include <cstring>
#define OK 0
using std::memcpy;
using std::strncpy;
using std::strlen;
#define MIN(x,y) (((x)<(y))?(x):(y))
// (1)
WavPostData::WavPostData(const void *p, size_t size)
: pData((const char *)p), dataSize(size), pos(0) {
}
void WavPostData::readReset() {
pos = 0;
}
int WavPostData::read(char *buf, size_t len, size_t *pReadLen) { // (2)
*pReadLen = MIN(len, dataSize - pos);
memcpy(buf, pData + pos, *pReadLen);
pos += *pReadLen;
return OK;
}
int WavPostData::getDataType(char *type, size_t maxTypeLen) { // (3)
strncpy(type, "audio/wav", maxTypeLen - 1);
type[maxTypeLen - 1] = '\0';
return OK;
}
bool WavPostData::getIsChunked() {
return false;
}
size_t WavPostData::getDataLen() {
return dataSize;
}
bool WavPostData::getHeader(char *header, size_t maxHeaderLen) {
return false;
}
