2017年06月23日

音を利用する 5 〜mbed マイコンでの応用:実装編〜 (終)

はてなブックマークに登録

この記事の内容

前回の記事では mbed LPC1768 を使って音声信号の入出力と分析を試みました。今回はその内容をもとに実用性の見込めるものを作ってみます。

※記事では音の分析に以下のアプリケーションを利用しています

1. 音をトリガーに通知を行うしくみを作る

まず、所定の音を鳴らすとそれに反応して所定の連絡先へ通知を行うしくみを想定しました。装置が屋内で常時集音と分析を行い、所定の周波数の連続性のある音声信号を検知するとインターネット経由で所定の対象へ通知を送るイメージです。次のように考えました。

  • 音は電磁波に比べより小さなエネルギーで発生させることができるため何らかの処理を指示するためのトリガーとしての有用性が見込まれる
  • 音は人間の官能に直結しており扱いが比較的容易であるため他の選択肢が閉ざされた状況において意思を伝えるための最後の手段となり得る
  • マイコンは常時稼働を前提とする情報処理装置の構成要素として好適でありこうした使い方は「IoT」そのものでもある

使途はいろいろ考えられそうですが、たとえば日常的に危険の潜在する高齢者世帯や独居世帯において非常時に SOS を発信するための用心棒としても有用でしょう。

どのような音を使うか

通知のトリガーとする音には所定の周波数と振幅の維持が求められます。もっとも簡単にそのような音を出せる身近な道具として「笛」に目を向け 5 種類の製品を試してみました。それぞれに特長がありこれらの笛製品の音をすべてトリガーとして利用したいと考えました。

A&F 社製 サイレントホイッスル 犬笛

  • A&F エイアンドエフ サイレントホイッスル - www.aandfstore.com
    人に聞こえる可聴領域の低音と犬にしか聞こえない超音波領域(16000〜22000Hz以上)の 高音を出すことができる軽量なアルミ合金から削り出して作った、たった7gの犬笛です
  • A&F エイアンドエフ サイレント ホイッスル - www.yodobashi.com
  • 長所
    • 日常的な環境音とはほとんど競合しない高周波の音を簡単に出力できる
    • 振幅の大きな音を「静か」に鳴らすことが可能
    • 少ない息で大きく長く音を出せる
    • 今回の中ではもっとも遠くまで音が伝わる
  • 備考
    • 価格が高め(実売¥2,000前後)
    • 犬笛は音が人間に聞こえにくい性質上一般的な防災・避難用途には適さない ※この製品自体は可聴音域へ切り替えが可能
サンプル(強い息:音量注意)
ピークは 10000Hz 付近
周波数分布

コクヨ社製 防災用救助笛

  • 防災用救助笛ツインウェーブ - 救助・避難用品 - www.kokuyo-st.co.jp
    ツインウェーブは人の耳に最も良く聞こえる約3000Hz〜5000Hzの周波数の中で、2種類の波長を出すため、より広範囲な人への注意喚起が可能となります。 小さな息でも大きな音を出すことができます。
  • コクヨ 防災用救助笛 防災の達人 ツインウェーブ オレンジ DRK-WS1YR - www.amazon.co.jp
  • 特別対談 山村武彦氏×鈴木松美氏(提供:コクヨ S&T 株式会社) - www.webside.jp
    山村氏 この「ツインウェーブ」ですが、一見、笛とはわかりませんね。・・・
    いつでも、どこでも、誰でもが持ち歩けるというのは、非常に大きなポイントだと思います。・・・
    鈴木氏 ひとつの音だけではなく、2種類、3種類の音が出せれば、より助けを求めやすいのではないかと思います。 ツインウェーブは、3200ヘルツと4800ヘルツの音を同時に出します。・・・
    鈴木氏 ツインウェーブは、通常で700〜800メートル先までは音が届きますから、エレベータで閉じ込められた場合なども有効です。 さらに、軽い息で大きな音を鳴らせるので、お年寄りにも最適です。力がなくても同じ周波数が出せるように、ずいぶん苦労しました。息のエネルギーをほとんど音に変換して、非常に効率がいいんです。 山村氏 この笛の効果は、これからの防災対策に欠かせないように思います。高齢者でも、 寝たきりの人でも、笛さえ吹ければ自らの居場所を知らせることができます。
  • 長所
    • 少ない息でより大きな音を出せる
    • プラスティック製かつ蓋つきで小さく携帯性に優れる
    • コストパフォーマンスが高い(実売¥400前後)
  • 備考
    • 手元での確認では製品名の由来でもある「2種類の波長」はあまりはっきりと観察できなかった。吹き方の影響?
サンプル(強い息:音量注意)
ピークは 3200Hz 付近
周波数分布

モルテン社製 電子ホイッスル

サンプル(音量注意)
ピークは 3800Hz 付近
周波数分布

レモン社製 緊急用呼子笛

  • 緊急用呼子笛 - lemon.gr.jp
  • 緊急用 呼子笛 885800 - www.amazon.co.jp
    携帯に便利なペンダントタイプの緊急用呼子笛です。 身元確認のためのIDカードが付いています。
  • 長所
    • 少ない息で大きく長く音を出せる
    • スリムで軽く携帯しやすい
    • コストパフォーマンスがきわめて高い(実売¥100〜200程度)
  • 備考
    • 「アルミ製」だが剛性に若干の不安あり
サンプル(強い息:音量注意)
ピークは 4300Hz 付近
周波数分布

一般的なホイッスル(メーカー不明)

  • 長所
    • 百均店で買えるなど入手性がよく紛失してもダメージが少ない
    • スタンダードであるため一見して笛と認識でき誰でも簡単に扱える
  • 備考
    • 音量の確保と持続に多くの息が必要
サンプル(強い息:音量注意)
ピークは 2300Hz 付近
周波数分布

どのように通知を行うか

緊急連絡用途を想定すると通知は複数の方法を併用することが好ましいと考えました。今回は以下のみっつの方法を想定しサービスの選定を行いました。

電話機への通知

電話はもっとも手早い通知手段でしょう。電話連絡を手近に扱えるサービスとして米国発の「Twilio」を知りこれを利用することにしました。所定の相手に対し所定の音声メッセージを流すことも簡単に実現できますが、ここでは電話機をコールするだけで受け手は何ごとかの発生を察知できるためコストのかかる音声通話機能を利用する必要はないかもしれません。

Twilio

  • Twilio - APIs for Text Messaging, VoIP & Voice in the Cloud - www.twilio.com
  • Twilio for KDDI Web Communications | クラウド電話API - twilio.kddi-web.com
    Twilio は開発者向けのプラットフォームサービス、たった数行のコードを書くだけでネットと電話・SMS をつなぎます。ネット上の仕組みを電話から操作したり、ネット上の情報を電話や SMS に通知することができるようになります。
  • API リファレンス - 全Twilio APIおよびSDKの詳細リファレンス - jp.twilio.com
    Twilio の REST API を使って、ユーザー アカウント、電話番号、通話、 テキスト メッセージ、録音のメタデータにクエリを送信することができます。 発信通話を開始したりテキスト メッセージを送信したりすることができます。
           :
  • 料金 - twilio.kddi-web.com
    電話番号料 1ヶ月あたり
    050 番号 100 円(税別) 108 円(税込)
           :
    発信料 1 分あたり
           :
    携帯電話宛(070 / 080 / 090 番号) 15 円(税別) 16.2 円(税込)
           :

サインアップするとトライアルアカウントとして試用ポイントが提供され(額は非公開)、同ポイントが尽きるとフルアカウントへのアップグレードとクレジットカード番号の登録を促されるしくみです。このポイントで 050 番号を一件取得しました。プログラムから簡単に電話連絡を扱えるのは便利なのでトライアル終了後も利用を継続したいと考えています。ちなみにコールだけなら上記の「発信料」は発生しません。

メッセージプッシュによる通知

スマホ・デスクトップ(ブラウザ)へのプッシュ通知も行います。メジャーな Pushover や Pushbullet はそれぞれ IFTTT に専用チャネルを持っていることもあり便利に扱えます。Pushover は専用アドレスへのメール送信によって通知処理をキックすることもできます。

メールでの通知

手元で使い慣れた SendGrid を利用してメールでの通知を行います。

試作 1

以下の要領で試作を行いました。

  • 使用したソフトウェア資産
  • インターネット接続の利用に伴い装置に以下のアダプタを追加
  • 処理の流れ
    1. サンプリングレート 33330Hz, FFT サイズ 1024 で集音と周波数分析をループ実行する
    2. FFT 処理において次の帯域のピーク周波数が検知されれば 1. を中断し 3. へ移行する
      ただしノイズによる影響を抑えるため所定の回数以上の連続検知を条件とする
      • 9800Hz〜16000Hz(犬笛系)
      • 2000Hz〜4500Hz(ホイッスル全般)
    3. 前回の通知から所定の時間が経過していれば 4. へ。
      経過していなければ 1. へ(通知の頻発を回避)
    4. Google ドライブ上の所定のエントリに対する GET リクエストを試行。
      レスポンスコードが 200 なら 5. へ
      それ以外なら 1. へ(通知の無効化をリモートから可能とするための措置)
      • 所定のアカウントの Google ドライブ上のエントリへの第三者からのアクセスにおいては、当該エントリの共有がオンの状態なら 200 が返され、共有オフの状態なら 302 が返される。この仕様を低コストで取り回せる簡易フラグとして利用
    5. Twilio へ手持ちの電話のコールをリクエスト
      • 今回はコールのみを目的とし短時間でタイムアウトさせる
    6. SendGrid へ手持ち Gmail アドレスへのメール通知をリクエスト
      • Gmail のフィルタ設定により当該メールは Pushover 通知キック用のアドレスへ転送
    7. ふたたび 1. へ

動作の様子

動画:2分31秒  ※音量注意

ソースコード

Sonic06_FFT3

  • main.cpp
    
    // 所定の周波数信号を所定の回数以上連続検知すると通知を行う
    
    #include "mbed.h"
    #include "rtos.h"
    #include "EthernetInterface.h"
    #include "HTTPClient.h"
    #include "FastAnalogIn.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    //#define USE_DHCP
    #if !defined(USE_DHCP)
    #define IP      "192.168.0.113"
    #define GATEWAY "192.168.0.1"
    #define MASK    "255.255.255.0"
    #endif
    
    #define SAMPLE_RATE 33330
    // n >= 2, n = power of 2
    #define FFTSIZE 1024
    // length of ip >= 2+sqrt(n/2)
    const int IP_LEN = 4 + sqrt((double)(FFTSIZE/2));
    // 当該サンプリングレートでのサンプリング間隔
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    
    // 通知の最短間隔秒
    #define INTERVAL_NOTIFICATION 180
    
    // 犬笛系周波数帯
    #define FREQ_LOW1   9800
    #define FREQ_HIGH1 16000
    
    // ホイッスル系周波数帯
    #define FREQ_LOW2  2000
    #define FREQ_HIGH2 4500
    
    // 対象周波数連続検知回数閾値
    #define MAXHITCOUNT 20
    
    // for Twilio
    #define TWILIO_SID  "AC1f7aa23e****************"
    #define TWILIO_AUTH "ef3638ca7*****************"
    #define URL_POSTDATA_TWILIO \
      "https://api.twilio.com/2010-04-01/Accounts/" TWILIO_SID "/Calls"
    #define REQUEST_HEADER_FOR_TWILIO \
      "Content-Type: "       "application/x-www-form-urlencoded"   "\r\n"
    #define POSTDATA_TWILIO \
      "To=%2B8190********" "&" \
      "From=%2B8150********" "&"  \
      "Url=http://demo.twilio.com/docs/voice.xml"  "&" \
      "Timeout=5"
    
    // for SendGrid
    #define SENDGRID_APIKEY "SG.UNFbQr_DQRmu8J********"
    #define REQUEST_HEADER_FOR_SENDGRID \
      "Authorization: Bearer " SENDGRID_APIKEY "\r\n" \
      "Content-Type: application/x-www-form-urlencoded\r\n"
    #define URL_POSTDATA_SENDGRID "https://api.sendgrid.com/api/mail.send.json"
    #define MAILTO    "********@gmail.com"
    #define MAILFROM  "mkttanabe2012@gmail.com"
    #define MAILSUBJECT "** ALERT [%s] **"
    #define MAILBODY  "%dHz"
    
    // Google ドライブ上のフラグエントリ URL
    #define URL_FLAGFILE \
      "https://docs.google.com/spreadsheets/d/1FR_RaR_AsyPAsIYU****************"
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick = 0;
    FastAnalogIn mic(p20);
    AnalogOut speaker(p18);
    DigitalOut led(LED4);
    
    osThreadId mainThreadID;
    EthernetInterface eth;
    HTTPClient http;
    
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    void setDummyTime() {
      struct tm t;
      t.tm_sec = t.tm_min = t.tm_hour = t.tm_mday = t.tm_mon = 1;
      t.tm_year = 100;
      time_t seconds = mktime(&t);
      set_time(seconds);
    }
    
    int initNetwork() {
      int sts;
      led = 1;
      dbg("start eth.init\r\n" );
    #ifdef USE_DHCP
      sts = eth.init();
    #else
      sts = eth.init(IP, MASK, GATEWAY);
    #endif
      if (sts != 0) {
        dbg("ech.init error!\r\n" );
        return -1;
      }
      dbg("start eth.connect\r\n" );
      sts = eth.connect();
      if (sts != 0) {
        dbg("eth.connect error!\r\n" );
        return -2;
      }
      dbg("my IP Address = %s\r\n", eth.getIPAddress());
      led = 0;
      return 0;
    }
    
    // Googleドライブ上の所定のフラグエントリをチェック
    int checkFlagFile() {
      char buf[8];
      for (int i = 0; i < 6; i++) {
        int sts = http.get(URL_FLAGFILE, buf, sizeof(buf),
              HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          int rc = http.getHTTPResponseCode();
          // 共有オンなら 200, オフなら 302
          dbg("Flag is %s\r\n", 
            (rc == 200) ? "enabled" :
            (rc == 302) ? "disabled" : "?????");
          return rc;
        }
        dbg("sts=%d retrying..\r\n", sts);
        wait(5);
      }
      return -1;
    }
    
    int httpsPost(const char *targetUrl,
          const char *postData,
          const char *optHeader,
          const char *authId,
          const char *authPass) {
      int sts;
      char buf[64];
      HTTPText dataToPost((char*)postData);
      HTTPText resData(buf, sizeof(buf));
      http.setHeader(optHeader);
    
      for (int i = 0; i < 3; i++) {
        dbg("request to [%s]..\r\n", targetUrl);
        http.basicAuth(authId, authPass);
        sts = http.post(targetUrl, dataToPost, &resData, HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          dbg("received HTTPS response OK\r\n");
          dbg("head of data [%s]\r\n", buf);
          dbg("done\r\n\r\n");
          break;
        } else {
          dbg("HTTPS request error=%d, status=%d\r\n", sts, http.getHTTPResponseCode());
          dbg("head of data [%s]\r\n", buf);
          dbg("sts=%d retrying..\r\n", sts);
          wait(5);
        }
      }
      return sts;
    }
    
    int doFFT(double *FFTBuffer) {
      int ip[IP_LEN];
      double *w = (double*)malloc(sizeof(double) * (FFTSIZE / 2));
      ip[0] = 0;
      rdft(FFTSIZE, 1, FFTBuffer, ip, w);
    
      double maxAmp = 0;
      int index = 0;
      int endIdx = FFTSIZE/2;
      for (int i = 0; i < endIdx ; i++) {
        double a = FFTBuffer[i*2]; // 実部
        double b = FFTBuffer[i*2 + 1]; // 虚部
        // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
        double r = sqrt(a*a + b*b);
        if (r > maxAmp) {
          maxAmp = r;
          index = i;
        }
      }
      float freq = index * SAMPLE_RATE / FFTSIZE;
      free(w);
      return (int)freq;
    }
    
    // サンプリング
    void flip() {
      // AnalogIn.read() の値範囲は 0〜1.0 につき中心値を 0 に
      recBufp[countTick++] = mic.read() - 0.5;
      if (countTick == FFTSIZE) {
        tick.detach();
        countTick = 0;
        osSignalSet(mainThreadID, 1); // 集音完了通知
      }
    }
    
    // 通知
    int checkAndNotify(int freq, time_t *lastNotificationTime) {
      int pastSeconds = time(NULL) - *lastNotificationTime;
      // 前回の通知から所定の時間が経過の場合のみ次処理へ
      if (pastSeconds > INTERVAL_NOTIFICATION) {
        // 通信処理のために 集音・FFT用バッファを一旦開放
        free(recBufp);
        recBufp = NULL;
        // Google ドライブ上の所定のフラグエントリをチェック
        // 共有がオンなら 200 が返り オフなら が返る
        dbg("checking flagFile..\r\n");
        int rc = checkFlagFile();
        dbg("rc=%d\r\n", rc);
        if (rc == 200) {
          for (int i = 0; i < 8; i++) {
            led = !led;
            wait_ms(200);
          }
          // Twilio で携帯電話をコール
          httpsPost(URL_POSTDATA_TWILIO, POSTDATA_TWILIO,
            REQUEST_HEADER_FOR_TWILIO, TWILIO_SID, TWILIO_AUTH);
    
          // SendGrid でメール送信
          char b[256];
          snprintf(b, sizeof(b), "from=" MAILFROM
              "&to=" MAILTO "&subject=" MAILSUBJECT
              "&text=" MAILBODY, eth.getIPAddress(), freq);
          dbg("mes=[%s]\r\n", b);
          
          httpsPost(URL_POSTDATA_SENDGRID, b,
            REQUEST_HEADER_FOR_SENDGRID, NULL, NULL);
    
          // 今回通知時刻を保持
          *lastNotificationTime = time(NULL);
        }
        recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
        return 1;
      }
      // 前回の発呼から所定の時間が経過していない
      dbg("pastSeconds=%d..\r\n", pastSeconds);   
      return 0;
    }
    
    int main()
    {
      mainThreadID = osThreadGetId();
      setDummyTime();
      initNetwork();
      // 集音用兼FFT用バッファ
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      time_t lastNotificationTime = 0;
      dbg("sampleRate=%dHz interval=%fsec\r\n", SAMPLE_RATE, SEC_PER_SAMPLEPOINT);
      
      tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      // 対象周波数信号検出数
      int hitCount = 0;
    
      while (1) {
        // サンプリング完了まで待機
        osSignalWait(1, osWaitForever);
        // ピーク周波数を得る
        int freq = doFFT(recBufp);
        if (freq > FREQ_LOW1 && freq <= FREQ_HIGH1 ||
          freq > FREQ_LOW2 && freq <= FREQ_HIGH2) {
          dbg("detected! %dHz\r\n", freq);
          led = 1;
          // 所定回数以上連続検知の場合のみ通知処理へ
          if (hitCount++ > MAXHITCOUNT) {
            hitCount = 0;
            checkAndNotify(freq, &lastNotificationTime);
          }
        } else {
          if (hitCount > 0) {
            dbg("ignored..\r\n");
          }
          hitCount = 0;
          led = 0;
        }
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

試作 1 の問題点について

試作 1 では集音した信号の中でもっとも振幅の大きい周波数(ピーク周波数)を評価しており、また、対象範囲の周波数が FFT 処理において所定の回数以上 "連続して" 検知されることを判定条件としています。そのため周辺音の大きな環境では対象音を取りこぼす可能性があり、装置・音源間の距離が判定に影響を及ぼす度合いも少なくありません。この点の改善が必要と考えました。

下の動画には静かな状況と騒がしい状況での装置の反応を確認した様子を収めています。このように周囲の音が大きいと負けてしまいます。

動画:41秒  ※音量注意!

(似た話:Amazon Echo が反応しないので大声で怒鳴る人:Youtube より)

試作 2

以下の内容でプログラムの修正を行いました。

  1. ピーク周波数ベースの判定を変更
    • ピーク周波数の参照に代え信号に含まれる対象範囲の周波数成分を能動的にチェックする。これにより周辺音による干渉を抑えられる
  2. 検知の考え方
    • 上記のチェックにおいて所定のレベル以上の振幅値を伴うものの発見をもって検知とみなす。これにより対象音が微弱でもピックアップが可能となる
  3. 連続性の判定方法
    • 音の連続性の判定は「対象周波数が所定の回数以上続けてピーク検出されること」を条件としていたが、これを「前回の検出から所定時間内の再検出が所定の回数以上繰り返されること」に変更する。これにより対象音が途切れがちでも切り捨ての可能性を抑えられる
  4. 対象周波数範囲の見直し
    • 従来は 2000Hz〜4500Hz(ホイッスル全般)および 9800Hz〜16000Hz(犬笛系)のふたつのレンジを対象としていたが、前者は 基音として日常生活に登場する機会が少なくないため混乱につながるケースも想定される。そこで「上音」の存在に注目した
      • 倍音 - wikipedia
        上音 音を正弦波に分解したときに、最も低い周波数である基音以外の成分を 上音(じょうおん、overtone)という。この上音には倍音でない音も含まれる。 倍音は、基音の(2以上の)整数倍の周波数の上音であると言い換えることができる。
      • どのような音を使うか」項に掲載の各音源の周波数分布(スペクトル)図を参照すると倍音を含む上音が高周波領域まで豊かに鳴っている様子が見てとれる。抜粋を以下に示す。※図の縦軸最大値は 15kHz
      • 上記 1, 2 の措置により振幅の小さな成分であっても細かく検知可能となる背景もあり、実験的に 10000Hz 以上の音を対象とする改訂を加えた。サンプリングレート 33330Hz で集音しているため理論上 16600Hz 付近まで拾うことが可能  →サンプリング定理
        使用しているマイクモジュールのカバーする音域は公称「100Hz〜15kHz」 →データシート
      • むろん周囲の生活音等が 10000Hz 以上の上音を発生させることもあるが、意図的なものでない限り上記 3 の連続性チェックをすり抜ける可能性は実用上低いと考えられる

動作の様子

動画:2分3秒  ※音量注意!

ソースコード

(利用するプッシュ通知サービスを Pushover から Pushbullet に変更しています)

Sonic06_FFT3

  • main.cpp
    
    // 所定の周波数信号を所定の回数以上連続検知すると通知を行う
    // 改訂版:連続性のある 10kHz 以上の周波数成分をターゲットとする
    
    #include "mbed.h"
    #include "rtos.h"
    #include "EthernetInterface.h"
    #include "HTTPClient.h"
    #include "FastAnalogIn.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    //#define USE_DHCP
    #if !defined(USE_DHCP)
    #define IP      "192.168.0.113"
    #define GATEWAY "192.168.0.1"
    #define MASK    "255.255.255.0"
    #endif
    
    #define SAMPLE_RATE 33330
    // n >= 2, n = power of 2
    #define FFTSIZE 1024
    // length of ip >= 2+sqrt(n/2)
    const int IP_LEN = 4 + sqrt((double)(FFTSIZE/2));
    // 当該サンプリングレートでのサンプリング間隔
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    
    // 通知の最短間隔秒
    #define INTERVAL_NOTIFICATION 180
    
    #define MAXHITCOUNT 30
    short indexOf10000Hz;
    
    // for Twilio
    #define TWILIO_SID  "AC1f7aa23e****************""
    #define TWILIO_AUTH "ef3638ca7****************""
    #define URL_POSTDATA_TWILIO \
        "https://api.twilio.com/2010-04-01/Accounts/" TWILIO_SID "/Calls"
    #define REQUEST_HEADER_FOR_TWILIO \
      "Content-Type: "       "application/x-www-form-urlencoded"   "\r\n"
    #define POSTDATA_TWILIO \
      "To=%2B8190********"" "&" \
      "From=%2B8150********"" "&"  \
      "Url=http://demo.twilio.com/docs/voice.xml"  "&" \
      "Timeout=5"
    
    // for SendGrid
    #define SENDGRID_APIKEY "SG.UNFbQr_DQRmu8J********"
    #define REQUEST_HEADER_FOR_SENDGRID \
      "Authorization: Bearer " SENDGRID_APIKEY "\r\n" \
      "Content-Type: application/x-www-form-urlencoded\r\n"
    #define URL_POSTDATA_SENDGRID "https://api.sendgrid.com/api/mail.send.json"
    #define MAILTO    "********@gmail.com"
    #define MAILFROM  "mkttanabe2012@gmail.com"
    #define MAILSUBJECT "** ALERT [%s] **"
    #define MAILBODY  "%dHz"
    
    // for IFTTT
    #define IFTTT_KEY "dSIwDguVEYfZ1SgiISL"********@"
    #define IFTTT_EVENT_NAME "ALERT"
    #define REQUEST_HEADER_FOR_IFTTT \
      "Content-Type: application/json\r\n"
    #define URL_POSTDATA_IFTTT  "https://maker.ifttt.com/trigger/" \
          IFTTT_EVENT_NAME "/with/key/" IFTTT_KEY
    
    // Google ドライブ上のフラグエントリ URL
    #define URL_FLAGFILE \
      "https://docs.google.com/spreadsheets/d/1FR_RaR_AsyPAsIYU****************"
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick = 0;
    FastAnalogIn mic(p20);
    AnalogOut speaker(p18);
    DigitalOut led(LED4);
    
    osThreadId mainThreadID;
    EthernetInterface eth;
    HTTPClient http;
    
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    void setDummyTime() {
      struct tm t;
      t.tm_sec = t.tm_min = t.tm_hour = t.tm_mday = t.tm_mon = 1;
      t.tm_year = 100;
      time_t seconds = mktime(&t);
      set_time(seconds);
    }
    
    int initNetwork() {
      int sts;
      led = 1;
      dbg("start eth.init\r\n" );
    #ifdef USE_DHCP
      sts = eth.init();
    #else
      sts = eth.init(IP, MASK, GATEWAY);
    #endif
      if (sts != 0) {
        dbg("ech.init error!\r\n" );
        return -1;
      }
      dbg("start eth.connect\r\n" );
      sts = eth.connect();
      if (sts != 0) {
        dbg("eth.connect error!\r\n" );
        return -2;
      }
      dbg("my IP Address = %s\r\n", eth.getIPAddress());
      led = 0;
      return 0;
    }
    
    // Googleドライブ上の所定のフラグエントリをチェック
    int checkFlagFile() {
      char buf[8];
      for (int i = 0; i < 6; i++) {
        int sts = http.get(URL_FLAGFILE, buf, sizeof(buf),
              HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          int rc = http.getHTTPResponseCode();
          // 共有オンなら 200, オフなら 302
          dbg("Flag is %s\r\n", 
            (rc == 200) ? "enabled" :
            (rc == 302) ? "disabled" : "?????");
          return rc;
        }
        dbg("sts=%d retrying..\r\n", sts);
        wait(5);
      }
      return -1;
    }
    
    int httpsPost(const char *targetUrl,
          const char *postData,
          const char *optHeader,
          const char *authId,
          const char *authPass) {
      int sts;
      char buf[64];
      HTTPText dataToPost((char*)postData);
      HTTPText resData(buf, sizeof(buf));
      http.setHeader(optHeader);
      
      for (int i = 0; i < 3; i++) {
        //dbg("postData=[%s]\r\n", postData);
        dbg("request to [%s]..\r\n", targetUrl);
        http.basicAuth(authId, authPass);
        sts = http.post(targetUrl, dataToPost, &resData, HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          dbg("received HTTPS response OK\r\n");
          dbg("head of data [%s]\r\n", buf);
          dbg("done\r\n\r\n");
          break;
        } else {
          dbg("HTTPS request error=%d, status=%d\r\n", sts, http.getHTTPResponseCode());
          dbg("head of data [%s]\r\n", buf);
          dbg("sts=%d retrying..\r\n", sts);
          wait(5);
        }
      }
      return sts;
    }
    
    int doFFT(double *FFTBuffer) {
      int ip[IP_LEN];
      double *w = (double*)malloc(sizeof(double) * (FFTSIZE / 2));
      ip[0] = 0;
      rdft(FFTSIZE, 1, FFTBuffer, ip, w);
      bool found = false;
      float freq;
      int endIdx = FFTSIZE/2;
      for (int i = endIdx - 1; i >= indexOf10000Hz; i--) {
        freq = i * SAMPLE_RATE / FFTSIZE;
        double a = FFTBuffer[i*2];
        double b = FFTBuffer[i*2 + 1];
        double mag = sqrt(a*a + b*b);
        if (mag > 0.2) {
          dbg("freq=%f mag=%f\r\n", freq, mag);
          found = true;
          break;
        }
      }
      free(w);
      return (found) ? (int)freq : 0;
    }
    
    // サンプリング
    void flip() {
      // AnalogIn.read() の値範囲は 0〜1.0 につき中心値を 0 に
      recBufp[countTick++] = mic.read() - 0.5;
      if (countTick == FFTSIZE) {
        tick.detach();
        countTick = 0;
        osSignalSet(mainThreadID, 1); // 集音完了通知
      }
    }
    
    // 通知
    int checkAndNotify(int freq, time_t *lastNotificationTime) {
      int pastSeconds = time(NULL) - *lastNotificationTime;
      // 前回の通知から所定の時間が経過の場合のみ次処理へ
      if (pastSeconds > INTERVAL_NOTIFICATION) {
        // 通信処理のために 集音・FFT用バッファを一旦開放
        free(recBufp);
        recBufp = NULL;
        // Google ドライブ上の所定のフラグエントリをチェック
        // 共有がオンなら 200 が返り オフなら が返る
        dbg("checking flagFile..\r\n");
        int rc = checkFlagFile();
        dbg("rc=%d\r\n", rc);
        if (rc == 200) {
          for (int i = 0; i < 8; i++) {
            led = !led;
            wait_ms(200);
          }
          // Twilio で携帯電話をコール
          httpsPost(URL_POSTDATA_TWILIO, POSTDATA_TWILIO,
            REQUEST_HEADER_FOR_TWILIO, TWILIO_SID, TWILIO_AUTH);
    
          // SendGrid でメール送信
          char b[256];
          snprintf(b, sizeof(b), "from=" MAILFROM
              "&to=" MAILTO "&subject=" MAILSUBJECT
              "&text=" MAILBODY, eth.getIPAddress(), freq);
          dbg("mes=[%s]\r\n", b);
          httpsPost(URL_POSTDATA_SENDGRID, b,
            REQUEST_HEADER_FOR_SENDGRID, NULL, NULL);
    
          // IFTTT 経由で Pushbullet 通知をキック
          snprintf(b, sizeof(b),
            "{\"value1\":\"ALERT [%s]\", \"value2\":\"%dHz\", \"value3\":\"\"}\r\n",
            eth.getIPAddress(), freq);
          httpsPost(URL_POSTDATA_IFTTT, b,
            REQUEST_HEADER_FOR_IFTTT, NULL, NULL);
    
          // 今回通知時刻を保持
          *lastNotificationTime = time(NULL);
        }
        recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
        return 1;
      }
      // 前回の発呼から所定の時間が経過していない
      dbg("pastSeconds=%d..\r\n", pastSeconds);   
      return 0;
    }
    
    int main()
    {
       dbg("**** TARGET FREQUENCY MODE ****\r\n");
       int endIdx = FFTSIZE/2 - 1;
       for (int i = 0; i <= endIdx; i++) {
        float freq0 = i * SAMPLE_RATE / FFTSIZE;
        // 10kHz 以上の高周波音をターゲットとする
        if (freq0 >= 10000) {
          indexOf10000Hz = i;
          float freq1 = endIdx * SAMPLE_RATE / FFTSIZE;
          dbg("idx0=%d freq=%f\r\nidx1=%d freq=%f\r\n",
              i, freq0, endIdx, freq1);
          break;
        }
      }
      mainThreadID = osThreadGetId();
      setDummyTime();
      initNetwork();
      // 集音用兼FFT用バッファ
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      time_t lastNotificationTime = 0;
      
      dbg("sampleRate=%dHz interval=%fsec\r\n", SAMPLE_RATE, SEC_PER_SAMPLEPOINT);
      
      tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      // 対象周波数信号検出数
      int hitCount = 0;
      time_t prevHitTime = 0;
      while (1) {
        osSignalWait(1, osWaitForever);
        // 監視対象とする周波数領域の成分が所定レベル以上の振幅ならその周波数、
        // そうでなければ 0 が返る
        int freq = doFFT(recBufp);
        if (freq != 0) {
          // 前回検出から1秒以内の再検出ならカウントアップ
          if (time(NULL) - prevHitTime <= 1) {
            // 所定の回数以上検知の場合は通知処理へ
            if (++hitCount >= MAXHITCOUNT) {
              dbg("checkAndNotify\r\n");
              checkAndNotify(freq, &lastNotificationTime);
              hitCount = 0;   
            }
          } else {
            hitCount = 1;
            dbg("restart\r\n");
          }
          prevHitTime = time(NULL);
        }
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

2. 着信音で遠隔操作を行うしくみを作る

自宅の固定電話機の着信音をトリガーに処理を行うしくみを考えました。次のイメージです。

  • 手持ちの携帯電話など所定の電話番号からのコールに対する固有の着信音を自宅固定電話へ設定しておく
  • 電話機の側に設置した装置は当該着信音によるコールを待ち続ける
  • 装置は所定回数のコールを連続して検知すると所定の機器への通電をオン/オフする
使途として防犯目的での屋内照明の遠隔操作などが考えられるでしょう。

過去の試みとの比較

手元で過去に何度か同様の遠隔操作を試みたことがあります。いずれもインターネット上のサービスを利用して装置へ通知を行い、それをトリガーに処理を行うものです。

これらはいずれもおおむね期待通りに動きましたが、一方で次のようなジレンマもありました。

  • 外部サービス側処理の遅延発生や一時停止の可能性を回避することはできない:問題 大
  • 遠隔操作にはインターネット接続環境が必須:問題 中
  • クライアントアプリ(Webブラウザを含む)の操作に手間がかかる:問題 小

それに対して、装置への通知に「音」を利用することには次のメリットが挙げられます。

  • 災害時等の特殊な状況以外での発着信上のトラブルの可能性はほぼ度外視できる
  • 発着信にはネット接続環境も特別なアプリケーションも不要
  • 発信電話番号にこだわらなければ携帯電話機を携行する必要もない

こういった特長を活かしうまく使えば有用なツールとなりそうです。

準備

現在自宅では固定電話機としてユニデン社製の「UCT-002」を使っています。

トリガーとする着信音として、この UCT-002 の「パターン2」を選びました。次のような音です。

  • 音の様子
  • 波形の概観(Audacity による)
  • 周波数分布
特徴をまとめてみます。

  • 1秒間発振 〜 2秒間無音 を繰り返す
  • 複数の周波数の音を小刻みに発振
  • mbed 上で前出のプログラムを使ってピーク周波数を確認すると以下の成分が検出された
    • 1204Hz
    • 1009Hz
    • 976Hz
    • 813Hz
    • 781Hz

試作

以上の内容をもとに次の要領で試作を行いました。

  • この電話機は装置への密接が可能であるためここでは常にピーク周波数に注目する
  • 上に挙げた 5種類の周波数成分を監視対象とする
  • 「対象周波数を所定回数以上連続検知すること」と「その後に 2秒程度の非検知が続くこと」をもって一回のコールと判断する
  • コールを 3回連続して認識した後にもう一度対象周波数を検知するとアクションを実行する
  • アクションの内容は AC電源機器への通電。前に作ったリレーつき電源ケーブルを mbed LPC1768 の p10 に接続し Hi / Low で機器への通電有無を切り替える
  • アクション発生時にはメールで通知する

動作の様子

動画:1分40秒  ※音量注意!

この動画では携帯電話から自宅固定電話をコールして照明の ON/OFF を行っています。

動画:1分24秒  ※音量注意!

PC 上で対象着信音をサンプリング〜改変したものを数種類鳴らし、装置がそれらには応じない様子を収めています。

ソースコード

Sonic09_FFT4

  • main.cpp
    
    // 所定パターンの電話着信音に反応してアクションを起こす
    // ユニデン社製 UCT-002 の着信音「パターン2」への対応版
    // 1秒発振〜2秒無音 の繰り返し
    
    #include "mbed.h"
    #include "rtos.h"
    #include "EthernetInterface.h"
    #include "HTTPClient.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    //#define USE_DHCP
    #if !defined(USE_DHCP)
    #define IP      "192.168.0.113"
    #define GATEWAY "192.168.0.1"
    #define MASK    "255.255.255.0"
    #endif
    
    #define SAMPLE_RATE 33330
    // n >= 2, n = power of 2
    #define FFTSIZE 1024
    // length of ip >= 2+sqrt(n/2)
    const int IP_LEN = 4 + sqrt((double)(FFTSIZE/2));
    
    // 発振音に含まれる周波数成分(監視対象)
    int tgtF[] = {1204, 1009, 976, 813, 781};
    #define isTargetFreq(F) \
      (F == tgtF[0] || F == tgtF[1] || F == tgtF[2] || F == tgtF[3] || F == tgtF[4])
    
    // for SendGrid
    #define SENDGRID_APIKEY "SG.UNFbQr_DQRmu8J********"
    #define REQUEST_HEADER_FOR_SENDGRID \
      "Authorization: Bearer " SENDGRID_APIKEY "\r\n" \
      "Content-Type: application/x-www-form-urlencoded\r\n"
    #define URL_POSTDATA_SENDGRID "https://api.sendgrid.com/api/mail.send.json"
    #define MAILTO    "********@gmail.com"
    #define MAILFROM  "mkttanabe2012@gmail.com"
    #define MAILSUBJECT "** Power %s **"
    #define MAILBODY  "callCount:%d"
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick;
    Timer timer;
    bool recDone;
    AnalogIn mic(p20);
    DigitalOut led(LED4);
    DigitalOut relay(p10); // リレーつき電源ケーブルを接続
    
    EthernetInterface eth;
    HTTPClient http;
    osThreadId mainThreadID;
    
    // 当該サンプリングレートでのサンプリング間隔
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    // 音声信号は実数データにつき実離散フーリエ変換(Real DFT)関数を使う
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    void setDummyTime() {
      struct tm t;
      t.tm_sec = t.tm_min = t.tm_hour = t.tm_mday = t.tm_mon = 1;
      t.tm_year = 100;
      time_t seconds = mktime(&t);
      set_time(seconds);
    }
    
    int initNetwork() {
      int sts;
      led = 1;
      dbg("start eth.init\r\n" );
    #ifdef USE_DHCP
      sts = eth.init();
    #else
      sts = eth.init(IP, MASK, GATEWAY);
    #endif
      if (sts != 0) {
        dbg("ech.init error!\r\n" );
        return -1;
      }
      dbg("start eth.connect\r\n" );
      sts = eth.connect();
      if (sts != 0) {
        dbg("eth.connect error!\r\n" );
        return -2;
      }
      dbg("my IP Address = %s\r\n", eth.getIPAddress());
      led = 0;
      return 0;
    }
    
    int httpsPost(const char *targetUrl,
          const char *postData,
          const char *optHeader,
          const char *authId,
          const char *authPass) {
      int sts;
      char buf[64];
      HTTPText dataToPost((char*)postData);
      HTTPText resData(buf, sizeof(buf));
      http.setHeader(optHeader);
      
      for (int i = 0; i < 3; i++) {
        //dbg("postData=[%s]\r\n", postData);
        dbg("request to [%s]..\r\n", targetUrl);
        http.basicAuth(authId, authPass);
        sts = http.post(targetUrl, dataToPost, &resData, HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          dbg("received HTTPS response OK\r\n");
          dbg("head of data [%s]\r\n", buf);
          dbg("done\r\n\r\n");
          break;
        } else {
          dbg("HTTPS request error=%d, status=%d\r\n", sts, http.getHTTPResponseCode());
          dbg("head of data [%s]\r\n", buf);
          dbg("sts=%d retrying..\r\n", sts);
          wait(5);
        }
      }
      return sts;
    }
    
    int doFFT(double *FFTBuffer) {
      int ip[IP_LEN];
      double *w = (double*)malloc(sizeof(double) * (FFTSIZE/2));
      ip[0] = 0;
      rdft(FFTSIZE, 1, FFTBuffer, ip, w);
    
      double maxAmp = 0;
      int index;
      for (int i = 0; i < FFTSIZE/2; i++) {
        double a = FFTBuffer[i*2]; // 実部
        double b = FFTBuffer[i*2 + 1]; // 虚部
        double r = sqrt(a*a + b*b);
        if (r > maxAmp) {
          maxAmp = r;
          index = i;
        }
      }
      double freq = index * SAMPLE_RATE / FFTSIZE;
      if (freq > 0 && maxAmp > 1.0) {
        //dbg("freq=%d mag=%f\r\n", (int)freq, maxAmp);
      }
      free(w);
      return (int)freq;
    }
    
    // サンプリング
    void flip() {
      // AnalogIn.read() の値範囲は 0〜1.0 につき中心値を 0 に
      recBufp[countTick++] = mic.read() - 0.5;
      if (countTick == FFTSIZE) {
      tick.detach();
      osSignalSet(mainThreadID, 1); // 集音完了通知
      }
    }
    
    // メール送信
    void sendMail(int callCount) {
      char b[128];
      snprintf(b, sizeof(b), "from=" MAILFROM
            "&to=" MAILTO "&subject=" MAILSUBJECT
            "&text=" MAILBODY, (relay == 1) ? "ON" : "OFF", callCount+1);
      httpsPost(URL_POSTDATA_SENDGRID, b,
            REQUEST_HEADER_FOR_SENDGRID, NULL, NULL);
    }
    
    int main() {
      time_t lastDetectedTime = 0, lastActionTime = 0;
      time_t lastNonDetectedTime = 0;
      int blankPeriod = 0, ringCount = 0, callCount = 0;
    
      relay = 0;
      setDummyTime();
      mainThreadID = osThreadGetId();
      initNetwork();
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      dbg("start\r\n");
    
      while (1) {
        countTick = 0;
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
        osSignalWait(1, osWaitForever);
        if (time(NULL) - lastDetectedTime >= 4) {
          // 4秒以上対象音を非検知ならコールカウンタをリセット
          callCount = 0;
        }
        int freq = doFFT(recBufp);
        if (isTargetFreq(freq)) {
          // 前回のアクションから所定の時間が経過していなければ受け流す
          if (lastActionTime > 0) {
            int w = time(NULL) - lastActionTime;
            if (w < 30) {
              dbg("pending.. %dsec\r\n", w);
              continue;
            }
          }
          ringCount++;
          if (callCount >= 3) {
            // アクション実行
            relay = !relay;
            dbg("Power %s\r\n", (relay == 1) ? "On" : "Off");
            free(recBufp);
            sendMail(callCount);
            recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
            callCount = ringCount = 0;
            lastActionTime = time(NULL);
          }
          lastDetectedTime = time(NULL);
        } else { // 非対象音
          blankPeriod += (time(NULL) - lastNonDetectedTime);
          if (blankPeriod > 30) {
            blankPeriod = 0;
          }
          lastNonDetectedTime = time(NULL);
          if (ringCount > 7) {
            if (callCount == 0 ||
              blankPeriod >= 2 && blankPeriod < 4) {
              callCount++;
              dbg("call count=%d\r\n", callCount);
              blankPeriod = 0;
            }
          }
          ringCount = 0;
        }
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

おわりに

音を題材に手元で行ってきた実験と試作の内容を 5 回に分けて紹介しました。音を媒体とする手法は旧くて新しい奥の深い世界です。新たな製品やサービス・アプリケーションを構築する際の道具立てのひとつとして、あるいは、他の方法がすべて利用できない場合の伏兵としても有用でしょう。とても興味深い経験でした。


(tanabe)
klab_gijutsu2 at 11:54│Comments(0)TrackBack(0)IoT 

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント