SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

IoTをかじってみよう

IoTをかじってみよう(7)
~mbedを使って音声認識でデバイスを制御する

サーバ側プログラム

 冒頭で解説した通り、今回は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全体を示します。

図11 mbed処理のフローチャート
図11 mbed処理のフローチャート
リスト1 mbedのプログラム(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)。

図12 状態遷移図
図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)。

リスト2-1 バイナリデータ送信用データ構造(WavPostData.h)
#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;
}
  • (1)コンストラクタで送るデータのポインタとサイズを受け取ります。また、どこまで送信したかを示すpos変数を0で初期化しています
  • (2)HttpClientは送信するデータを取得する際に、read()関数を呼び出します。第1引数が送信したいデータを格納する場所、第2引数が、第1引数で渡した領域の大きさ。第3は出力引数で、ここを通して読み込めた長さを返します。
  • (3)HttpClientからgetDataType()が呼び出されたら、コンテントタイプを返します。

次のページ
動作確認(ローカル・プロクシ)

この記事は参考になりましたか?

IoTをかじってみよう連載記事一覧

もっと読む

この記事の著者

花井 志生(ペンネーム 宇野るいも)(ハナイ シセイ(ペンネーム ウノルイモ))

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/9568 2016/08/10 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー