2017年06月29日

最近のPython-dev(2017-06)

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

バックナンバー:

PEP 546 -- Backport ssl.MemoryBIO and ssl.SSLObject to Python 2.7

2014年に Python 2.7 をセキュアな状態に保つため、過去に PEP 466 でセキュリティ関連の Python 3.4 の機能が Python 2.7 にバックポートされました。これには ssl モジュールも含まれており、 Python 2.7.9 からは TLS のホストを自動で検証するようになったりシステムの証明書ストアを利用できるようになりました。

今回の PEP 546 は、さらに Python 3.5 で追加された ssl.MemoryBIO と ssl.SSLObject も Python 2.7 にバックポートするものです。これらの API を利用すると、 socket をラップするのではなく、メモリ上のデータに対して TLS の処理ができるようになります。 Windows で winsock2 ではなく IOCP を使ったり、その他非同期IOフレームワークを使うときに socket と TLS を分離できることは重要です。

Python 2.7 自体のメンテは 2020 年に終了しますが、 pip が Python 2.7 のサポートを終了する時期は未定です。実際に今までも、pip はユーザーがほとんどゼロになるまでかなり長い間古い Python をサポートしてきました。

一方で pip が http クライアントとして使っている requests は、将来内部で非同期IOベースにしていくことを検討しています。そして Python 2.7 のためだけに複数の実装をメンテナンスすることには否定的です。

PEP 546 により、 pip と requests が Python 2.7 のサポートを完全に切る前でも、(今年後半にリリースされる) 2.7.14 以降のみのサポートに限定するという形で requests の内部に非同期IO化を実現する余地ができます。

実際にこのシナリオ通りに進まない可能性もあったので、本当に必要なのかどうか議論が白熱しましたが、バックポートが提案されているコードのサイズはCとPythonを足してほんの数百行でしかありません。

最終的に Python 2.7 のリリースマネージャーの Benjamin Peterson さんが、 PEP 466 のときにこのAPIがあったら一緒にバックポートされてたんだし、反対する強い理由は無いよね、といって Accept しました。

PEP 538 warning at startup: please remove it

先月僕が Accept した PEP 538 (C locale のとき LC_COLLATE を C.UTF-8 などに上書きする) が実装され取り込まれました。

さて、この新機能は、 locale の変更に成功したときにはユーザーに最初からUTF-8のロケールを設定するよう促すための warning を、失敗したときは C locale を使ってるから非ASCII文字で問題が出るよという warning を表示していました。

しかし、この warning が単純に鬱陶しいのと、子プロセスを起動して標準入出力を見るようなテストが幾つかのマイナーなプラットフォームで壊れてしまってメンテが面倒になってきました。

ということで、どちらの warning もデフォルトで無効になりました。環境変数に PYTHONCOERCECLOCALE=warn と設定したら有効になるのですが、まぁ誰も使わないでしょうね。

LC_COLLATE を変更して UTF-8 を使えるようにしても問題になることはめったに無いですし、 C locale で非ASCII文字の扱いに制限があるのは Python 3.6 と同じで何かが悪くなったわけは無いので、 warning が無くなることで困るユーザーはほとんど居ないと踏んでいます。

PEP 544 -- Protocols: Structural subtyping (static duck typing)

Python の typing モジュールに structural subtyping のサポートが追加されました。

簡単に説明すると、 typing.Protocol クラスを継承したクラスを定義すると、それがプロトコルになります。プロトコルクラスのメソッドと同じシグネチャのメソッドを持つクラスは、そのプロトコルクラスを継承していなくても、静的チェッカはサブクラスと判断するようになります。

PEP から分かりやすい例を引用します。次の例は、ユーザー定義型 BucketSizedIterable だと型チェッカに教えるために継承を使っています。

from typing import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

PEP 544 が承認されると、 Sized や Iterable が Protocol になるので、この Bucket は継承を外しても SizedIterable[int] の部分型だと型チェッカが判断するようになります。

個人的には、 ABC を継承すると起動が遅くなる(クラスの生成が遅くなる)ので継承しなくても良くなるのは嬉しい一方、最初から typing が ABC を使わないでいてくれたらもっと軽量化できたのになーという気持ちもあります。

typing のオーバーヘッドについては先月の Language Summit でも話題になったらしく、MLなどでも議論されているので、これから最適化オプションで削るなどの改善が提案されるかもしれません。

起動高速化

3月号で紹介したのですが、Pythonの起動を高速化するためのプルリクエストを2件出していて、片方 は3月中にマージされ、もう片方 を一昨日マージしました。特に後者は macOS 上での性能向上が顕著で、クリーンインストールされた Python で起動時間が30msくらいだったのが20msくらいに減ります。

site という、 (Python の -S オプションを使わなかったときに) 自動で読み込まれるモジュールがあり、これが sys.path というライブラリを import するときに探す場所などを準備しています。 pip とか apt-get とかでインストールしたモジュールがすぐに使えるようになるのはこの site のおかげです。

この site が、いくつかのディレクトリの場所を決定するために sysconfig という Python の環境情報を集めたライブラリを読み込んでいるのですが、それがそこそこ重いです。そしてさらに macOS の場合、 Framework 内のディレクトリ名 ("Python" だったり "Python3.7" だったり) を調べるために sysconfig から _sysconfigdata という、 Makefilepyconfig.h から大量の変数をかき集めた dict を持つモジュールを利用していました。

Framework 名を sys._framework に入れて _sysconfigdata の必要性をなくし、さらに sysconfig から必要なごく一部分だけを site 内にコピーすることで、起動時間の短縮だけでなく、 mac の場合は多分起動直後のメモリ使用量も1MB程度減っています。

また、 site からのモジュールの import を減らすことによる最適化は、そのモジュールが functools みたいによく使われるものであれば結局アプリケーションの起動時間には寄与しない事も多いのですが、 sysconfig を使うのは site 以外には pipdistutils などのパッケージング関連のツールやライブラリだけなので、今回の最適化は多くのアプリケーションの起動時間・メモリ使用量にもそのまま寄与するはずです。

songofacandy at 22:29
この記事のURLComments(0)TrackBack(0)Python 
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
この記事のURLComments(0)TrackBack(0)IoT 
2017年06月20日

音を利用する 4 〜mbed マイコンでの応用:準備編〜

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

この記事の内容

IoT 方面での応用を想定しマイコン系でも音に関する実験と試作を行いました。今回はその準備編です。ここではプラットフォームとして mbed LPC1768 ボードを使用しています。基本的な考え方は環境に依存しないため要素の多くは他の製品向けに読み替えることも可能でしょう。

準備 1. 部品を用意する

音声データの入出力用に以下の部品を用意しました。

マイクモジュールを mbed LPC1768 の p20(AnalogIn)に、オーディオアンプを p18(AnalogOut)に接続しました。これを実験に使います。

準備 2. mbed マイコンでの音声入出力処理を覚える

何はともあれ音声データの入出力方法からです。リファレンスを確認します。

  • AnalogIn - アナログ入力 - developer.mbed.org
    val = name.read();
    valには0(0V)〜1.0(3.3V)の数値(float)が代入されます。

    val = name.read_u16();
    valには0(0V)〜65535(0xffff, 3.3V)の数値が代入されます。(上位12ビットが有効)
    • AN10974 LPC176x/175x 12-bit ADC design guidelines(PDF) - www.nxp.com
      Page 3 of 19 より
      The LPC175x/6x family is based on the ARM Cortex M3 core, and includes a 12-bit Analog-to-Digital (ADC) module with input multiplexing among eight pins, conversion rates up to 200 kHz, and multiple result registers.
  • AnalogOut - アナログ出力 - developer.mbed.org
    name = 0.5;
    0(0V)〜1(3.3V)の数値(float)で出力を指定します。

    name.write_u16 = 1000;
    0(0V)〜65535(0xffff, 3.3V)の数値(unsigned short)で出力を指定します。(上位10ビットが有効)

試作 1

まず、ごく素朴な録音・再生プログラムを書いてみました。次の内容です。

  • サンプリングレートは 4000Hz
  • 入力信号を 4000Hz でサンプリングするための時間間隔 w を計算
    w = 1sec / 4000Hz = 0.00025sec = 0.25msec = 250μsec
  • アナログ入力から w 間隔で読み取った値をサンプリング用バッファの終端まで順次格納する
  • サンプリング用バッファに格納した値を w 間隔で順次アナログ出力へ書き込む 〜無限ループ

動作の様子

動画:20秒  ※音量注意

ソースコード

sonic01_mic

  • main.cpp
    
    #include "mbed.h"
    
    #define SAMPLE_RATE 4000
    #define BUFSIZE 7680
    
    AnalogIn mic(p20);
    AnalogOut speaker(p18);
    DigitalOut led(LED4);
    float recBuf[BUFSIZE];
    
    int main() {
      // 当該サンプリングレートでのサンプリング間隔
      float w = 1.0 / SAMPLE_RATE;
    
      // 録音開始までに LED を 3回点滅させる
      led = 1;
      for (int i = 0; i < 6; i++) {
        wait(0.5);
        led = !led;
      }
      // 録音
      for (int i = 0; i < BUFSIZE; i++) {
        recBuf[i] = mic.read();
        wait(w);
      }
      led = 0;
    
      // 再生
      while (1) {
        for (int i = 0; i < BUFSIZE; i++) {
          speaker = recBuf[i] ;
          wait(w);
        }
      }
      return 0;
    }
    
    

試作 1 の問題点について

このようにとても短いコードで簡単に音声データの入出力を実現できました。しかし、録音と再生を試していると微妙な違和感がありました。録音した音と再生される音の高さが違うような気がしたのです。このことを楽器を使って確認してみました。

再生音の高さがおかしい?

動画:21秒  ※音量注意

この動画では楽器の音を録音し再生時に合わせ弾きなどを行っています。なお、前掲の安価なスピーカーではやはり音質が貧弱なので、この動画以降ではアンプ内蔵のスピーカー(PHILIPS 社製 BT50)を AnalogOut ポートにつないで再生に使用しています。

問題の原因と対処方法

このおかしな現象の原因は試作 1 のコードにおいてサンプリング間隔を制御している wait() 関数にあります。mbed プログラミングにおいてきわめてカジュアルに使用されるこの関数のリファレンスをあらためて確認したところ「When you call wait your board's CPU will be busy in a loop waiting for the required time to pass」との記述があり、とりあえず以下の内容で精度の確認を行いました。

  • 次のみっつのパターンについて、インターバル 10μ秒から 250μ秒までの 1μ秒刻みのループ内でそれぞれ 1000回 wait() を呼び出し所要時間と理論時間の差違を計測する
    • wait() のみ
    • wait() + AnalogIn.read()
    • wait() + AnalogOut.write()
インターバル 250μ秒での結果は次の通りです。
  • wait() のみ
    • wait=250us 所要時間:250666us 理論時間:250000us 誤差:666us (0.266400%)
  • wait() + AnalogIn.read()
    • wait=250us 所要時間:271662us 理論時間:250000us 誤差:21662us (8.664801%)
  • wait() + AnalogOut.write()
    • wait=250us 所要時間:252363us 理論時間:250000us 誤差:2363us (0.945200%)

このように wait() + AnalogIn.read() で大幅な遅延が発生しています。一方、wait() + AnalogOut.write() では相対的に遅延が小さいため試作 1 のプログラムは結果として「低いレートでサンプリング」したデータを「高いレートで再生」する所作となっていました。上の検証でのインターバル 10〜250μ秒を横軸、所要時間の誤差の割合を縦軸にプロットしたグラフを以下に示します。

(クリックで可読大表示)

サンプリング間隔のコントロールに mbed 標準のタイマ割込み機構である Ticker を使って同じ検証を行いました。 インターバル 250μ秒での結果は次の通りです。

  • Analog read / write なし
    • ticker=250us 所要時間:249770us 理論時間:250000us 誤差:230us (0.092000%)
  • AnalogIn.read() 呼び出しつき
    • ticker=250us 所要時間:249791us 理論時間:250000us 誤差:209us (0.083600%)
  • AnalogOut.write() 呼び出しつき
    • ticker=250us 所要時間:249772us 理論時間:250000us 誤差:228us (0.091200%)

(クリックで可読大表示)

このように結果は wait() 使用時よりも圧倒的に良好です。その一方で、AnalogIn.read() 併用時のインターバル 25μ秒付近の「壁」の存在が目に止まります。手元の検証ではこの結果には明確な再現性がみられました。25μ秒以下の割込みを必要としないサンプリングレートを利用すれば大きな支障はなさそうですが、広く用いられるレート 44100Hz の場合には 1sec / 44100Hz ≒ 23μsec とサンプリング間隔が 25μ秒を割り込むことが微妙に残念でもあります。

一連の調査の過程で Erik Olieman 氏による FastAnalogIn ライブラリの存在を知りました。

所定のアナログ入力ポートに対する mbed 標準の AnalogIn.read() が、呼び出しの都度、当該ポートに対応する ADC チャネル経由で AD 変換を行う仕様(そのため遅い)であるのに対し、FastAnalogIn は ADC の BURST mode を利用して変換をバックグラウンドで最大 200kHzで繰り返し実行し read() が呼び出されると「最後にサンプリングした値」を直ちに返す内容で実装されています。 (※ 前掲の ADC ドキュメント中の "conversion rates up to 200 kHz" のくだりは BURST mode 下のこの所作を指すものと考えられる)

  • FastAnalogIn - a mercurial repository | mbed - developer.mbed.org
    When you read an AnalogIn object it will enable the corresponding ADC channel, depending on the implementation do either one or multiple measurements for more accuracy, and return that value to your program. This way the ADC is only active when it is required, and it is fairly straightforward. However the downside is, is that an ADC is relatively slow.
          :
    This library uses the 'burst' feature of the microcontroller. This allows the ADC on the background to perform the AD conversions without requiring intervention from the microcontroller's core. Also there are no interrupts used, so also your time-sensitive code is not affected.
          :
    • FastAnalogIn Class Reference - developer.mbed.org
      AnalogIn does a single conversion when you read a value (actually several conversions and it takes the median of that). This library runns the ADC conversion automatically in the background. When read is called, it immediatly returns the last sampled value.
            :
  • ADC_LPC1768 - John Kneen: Microcontrollers - 6. Selecting and triggering the analogue to digital conversion. - sites.google.com
    Bit 16 : BURST

    1 The AD converter does repeated conversions at up to 200 kHz, scanning (if necessary) through the pins selected by bits set to ones in the SEL field.
          :

計測用コードの中の AnalogIn をこの FastAnalogIn に変更して同様の計測を行ったところ 25μ秒以下のインターバルでの成績が劇的に改善されました。前掲のふたつのグラフの縦軸最大値が「100%」であったのに対し、下のグラフでは「1.00%」であることに要注目です。

(クリックで可読大表示)

試作 2

上の「対処方法」を反映した形で録音・再生プログラムを書き直しました。検証上の便宜を兼ねて次の内容としています。

  • 録音・再生に要素数 14000 の unsigned short 型配列を使用する
  • サンプリングレート 8000Hz, 16000Hz, 22000Hz, 30720Hz, 44100Hz の順に録音と再生を行う
  • レートが高くなるほど録音可能時間は短くなるが、再生はいずれも 8秒間程度繰り返し行う

動作の様子

動画:1分28秒  ※音量注意

この動画では各サンプリングレートにおいてキーボードの「ファ」の音(F5:698.456Hz)を録音し、その再生音を以前作成したピーク周波数表示プログラムと楽器用のチューナー(KORG 社製 AW-2G)で確認しています。

ソースコード

sonic07_mic3

  • main.cpp
    // サンプリングと再生
    // レート 8000,16000,22000,30720,44100Hz の順にサンプリングを行い
    // それぞれ 8秒程度ループ再生する
    // FastAnalogIn ライブラリを使用
    
    #include "mbed.h"
    #include "FastAnalogIn.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    #define BUFSIZE 14000
    
    Ticker tick;
    Timer timer;
    int countTick,interval_us, sampleTime;
    bool recDone;
    unsigned short *recBufp = NULL;
    AnalogOut speaker(p18);
    FastAnalogIn mic(p20);
    DigitalOut led(LED4);
    
    // サンプリング
    void flipRec() {
      recBufp[countTick++] = mic.read_u16() & 0xFFFC;
      if (countTick == BUFSIZE) {
        tick.detach();
        recDone = true;
      }
    }
    
    // 再生
    void flipPlay() {
      speaker.write_u16(recBufp[countTick++]);
      if (countTick == BUFSIZE) {
        countTick = 0;
      }
    }
    
    void recAndPlay(int sampleRate) {
      // 当該サンプリングレートでのサンプリング間隔 usec
      interval_us = 1.0 / sampleRate * 1000000;
      // 当該サンプリングレートでのサンプリング時間 usec
      sampleTime = BUFSIZE * interval_us / sizeof(short);
      dbg("rate=%5dHz irq=%dus time=%dms\r\n",
            sampleRate, interval_us, sampleTime/1000);
      
      // サンプリング開始までに LED を 3回点滅させる
      led = 1;
      for (int i = 0; i < 6; i++) {
        wait(0.4);
        led =! led;
      }
      // サンプリング
      countTick = 0;
      recDone = false;
      tick.attach_us(&flipRec, interval_us);
      while (!recDone) {
        wait(0.2);
      }
      led = 0;
      wait(1);
      // 8秒程度ループ再生
      countTick = 0;
      tick.attach_us(&flipPlay, interval_us);
      wait(8);
      tick.detach();
    }
    
    int main() {
      recBufp = (unsigned short*)malloc(BUFSIZE);
      recAndPlay(8000);
      recAndPlay(16000);
      recAndPlay(22000);
      recAndPlay(30720);
      recAndPlay(44100);
      dbg("done\r\n");
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    
    

準備 3. 任意の周波数の音波を生成・出力する

先般のAndroid 環境での試みと同様に手元の mbed マイコンでサイン波信号を生成・出力してみます。Android 版での到達点を踏襲できるのであまり手はかかりませんでした。

試作

以下の要領でプログラムを作成しました。

  • 400Hz〜14000Hz の範囲の 10種類の周波数のサイン波信号を順次生成する
  • サンプリングレートは 33330Hz(サンプリング間隔 30μ秒)とし、バッファ上に各周波数 100ミリ秒分の信号データを構築してそれぞれ 4秒間程度 AnalogOut ポートへ出力する
  • unsigned short 型配列を引数にとる AnalogOut.write_u16() の引数範囲は 0〜65535 であるため信号生成時に直流成分として 65535 / 2 を加える

動作の様子

動画:1分15秒  ※音量注意

この動画では二台のタブレット上のふたつのプログラムを使って信号の確認を行っています。

最初に 400Hz サイン波信号の収録された Youtube 動画を使って確認を行い、その後 mbed マイコンから 10種類の周波数のサイン波信号を順番に 4秒ずつ鳴らしています。

ソースコード

sonic08_sine2

  • main.cpp
    
    // サイン波音声信号を生成・出力する
    
    #include "mbed.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    #define SAMPLE_RATE 33330
    #define PI 3.141592653589793238462
    #define MAG 3000
    
    Ticker tick;
    int count = 0;
    const int BUFSIZE = SAMPLE_RATE / 10; // バッファサイズ 100ミリ秒分
    unsigned short waveBuf[BUFSIZE];
    // 当該サンプリングレートでのサンプリング間隔 秒
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    const float a0 = 65535 / 2;
    
    AnalogOut speaker(p18);
    
    // 出力
    void flip() {
       speaker.write_u16(waveBuf[count++]);
       (count >= BUFSIZE) ? count = 0 : 1;
    }
    
    void outputSineWave(int freq) {
      for (int i = 0; i < BUFSIZE; i++) {
        float currentSec = i * SEC_PER_SAMPLEPOINT;
        double val = a0 + MAG * sin(2.0*PI * freq * currentSec);
        waveBuf[i] = (unsigned short)val;
      }
      count = 0;
      tick.attach(&flip, SEC_PER_SAMPLEPOINT;
      wait(4); // 4秒程度出力
      tick.detach();  
    }
    
    int main() {
      dbg("interval=%fs\r\n", SEC_PER_SAMPLEPOINT);
      outputSineWave(400);
      outputSineWave(800);
      outputSineWave(1200);
      outputSineWave(1600);
      outputSineWave(2000);
      outputSineWave(4000);
      outputSineWave(8000);
      outputSineWave(10000);
      outputSineWave(12000);
      outputSineWave(14000);
      return 0;
    }
    
    

準備 4. 集音したデータの周波数分析を行う

準備編の最後に入力信号の分析を試みます。

大浦版 FFT について

ここでは高性能かつコンパクトな FFT ライブラリとして広く利用(NASA の例)されている大浦拓哉様(京都大学数理解析研究所 助教)によるライセンスフリーの汎用 FFT パッケージ(通称 大浦版 FFT)を使ってみることにしました。先般 Android 環境下で利用した JTransforms はこのパッケージを母体とするソフトウェアです。

  • 大浦 拓哉 (Ooura, Takuya) - 京都大学数理解析研究所 - www.kurims.kyoto-u.ac.jp
    • 汎用 FFT (高速 フーリエ/コサイン/サイン 変換) パッケージ
      一次元 DFT / DCT / DST

      詳細
      一次元,2の整数乗個のデータの離散 Fourier 変換, コサイン変換,サイン変換等を行います.このパッケージには C と Fortran の FFT コードが含まれます.
            :

      内容
      fft4g.c : FFT パッケージ in C - 高速版 (基数 4, 2)
            :

      ファイルの違い
      簡易版は作業用配列を一切使いません.高速版は三角関数表などの作業用配列を使います.高速版は簡易版に比べて高速かつ高精度.高速版の仕様はすべて同じ.

      パッケージ内のルーチン
      cdft() : 複素離散 Fourier 変換 rdft() : 実離散 Fourier 変換
            :

      ライセンス
      Copyright Takuya OOURA, 1996-2001

      このソースコードはフリーソフトです.商用目的を含め,このコードの使用, コピー,修正及び配布は自由に行って結構です.ただし,このコードの修正を 行った場合は,そのことを明記してください.
    • 音声信号は実数データにつき実離散フーリエ変換関数 rdft() を使用する。使い方はシンプル。FFT サイズを 2のべき乗の値とすることに注意。
      -------- Real DFT / Inverse of Real DFT --------

       [usage]
        <case1>
         ip[0] = 0; // first time only
         rdft(n, 1, a, ip, w);
        <case2>
         ip[0] = 0; // first time only
         rdft(n, -1, a, ip, w);

       [parameters]
        n     :data length (int)
            n >= 2, n = power of 2

        a[0...n-1]  :input/output data (double *)
            <case1>
             output data
              a[2*k] = R[k], 0<=k<n/2
              a[2*k+1] = I[k], 0<k<n/2
              a[1] = R[n/2]
            <case2>
             input data
              a[2*j] = R[j], 0<=j<n/2
              a[2*j+1] = I[j], 0<j<n/2
              a[1] = R[n/2]

        ip[0...*]   :work area for bit reversal (int *)
            length of ip >= 2+sqrt(n/2)
            strictly, 
            length of ip >= 
             2+(1<<(int)(log(n/2+0.5)/log(2))/2).
            ip[0],ip[1] are pointers of the cos/sin table.

        w[0...n/2-1]   :cos/sin table (double *)
            w[],ip[] are initialized if ip[0] == 0.

       [remark]
        Inverse of 
         rdft(n, 1, a, ip, w);
        is 
         rdft(n, -1, a, ip, w);
         for (j = 0; j <= n - 1; j++) {
          a[j] *= 2.0 / n;
         }

試作

レート 33330Hz でサンプリングした音声信号を mbed マイコン上の FFT 処理にかけてピーク周波数を求めコンソールへ連続出力する内容で試作を行いました。手元の確認では mbed LPC1768 のメモリ容量上 FFT サイズは 210 = 1024 が限界でありその影響らしきものが若干窺えるものの十分に実用水準にあると考えられます。

動作の様子

動画:1分33秒  ※音量注意

この動画では二台のタブレット上のふたつのプログラムを使ってテストを行っています。

右のタブレットから順次所定の周波数のサイン波信号を出力し、それを mbed マイコンと左側のタブレットでそれぞれ FFT 処理にかけ両者の結果を観察しています。

ソースコード

sonic05_FFT2

  • main.cpp
    
    // 大浦版 FFT ライブラリで入力音声信号のピーク周波数を得る
    
    #include "mbed.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    #define PI = 3.141592653589793238462;
    #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));
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick;
    Timer timer;
    bool recDone;
    AnalogIn mic(p20);
    DigitalOut led(LED4);
    
    // 当該サンプリングレートでのサンプリング間隔 秒
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    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]; // 虚部
        // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
        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();
        recDone = true;
      }
    }
    
    int main() {
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      while (1) {
        countTick = 0;
        recDone = false;
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
        while (!recDone) {
          wait_ms(100);
        }
        doFFT(recBufp);
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

(tanabe)
klab_gijutsu2 at 17:21
この記事のURLComments(0)TrackBack(0)IoT 
2017年06月15日

音を利用する 3 〜Android デバイスでの音波通信:実装編〜

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

この記事の内容

前回の記事では Android デバイス間で音波通信を行うための準備として次のみっつの要素に目を向けました。

今回はこれらを素材として音声信号でビットデータを送受信してみます。四段階に分け順次機能を加えながら試作を行います。

1. ビットデータを音声信号で送受信する 1

想定した内容

まず最初の試作に際し次の内容を想定しました。

  • 送信側は送信専用、受信側は受信専用とする
  • 送信側は任意の文字列を所定の規則に添ってサイン波信号に変換しオーディオ出力する
  • 受信側は集音中に所定の規則に添った音声信号を検知するとそこから文字列情報を復元し画面に表示する
  • 開発上の便宜のために可聴音を使用する

以下についてはとりあえず後まわしです。

  • 伝送効率
  • 誤りへの対策

周波数変調(FSK)について

所定のデータを音声信号に変換するために周波数変調方式を利用することにしました。次の考え方です。

  • ふたつの周波数 A, B に 0 と 1 を割り当てる
  • 元データのビット並びを上記 A, B に置き換えそれらを所定の時間間隔で切り替える

  • FSK - 「通信用語の基礎知識」より
    正弦波に対してディジタル信号で変調を行なういわゆるディジタル変調の方式の一つで、ディジタル値を正弦波の周波数に対応させて伝送する方式のこと。具体的にはディジタル値の1/0に対応させて2つの周波数を決め、入力されるディジタル信号に応じてそれぞれの周波数の正弦波を交互に送出することで実現させる。

試作

処理のイメージ

ざっくり以下の要領で実装を試みました。

  • 周波数変調においては 8000Hz を 0, 10000Hz を 1 として扱い 1 ビットあたりの発振持続時間を 100ミリ秒とする (よって伝送速度は 10bps)

  • 信号出力に際しては単一バッファ上に終始の完結したデータを構築するのではなく生成済みの 8000Hz, 10000Hz 信号を再生バッファに順次直接書き込む
  • 今回は参照していないが末尾に終端の符丁として 12000Hz の信号を 1秒間付与する
  • 左:送信側 右:受信側

動作の様子

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

ソースコード

送信側: sonic05

  • MainActivity.java
    
    /**
     *
     * sonic05
     *
     * 文字列をサイン波信号に置き換えて出力する
     * Lo -> 8000Hz, Hi -> 10000Hz
     * 対向の受信プログラムは sonic06
     *
     */
    
    package jp.klab.sonic05;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.EditText;
    import android.widget.ToggleButton;
    
    import java.io.UnsupportedEncodingException;
    import java.util.Arrays;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final float SEC_PER_SAMPLEPOINT = 1.0f / SAMPLE_RATE;
      private static final int AMP = 4000;
      private static final int ELMS_1SEC = SAMPLE_RATE;
      private static final int ELMS_100MSEC = SAMPLE_RATE/10;
    
      private static final int FREQ_LOW  = 8000;
      private static final int FREQ_HI  = 10000;
      private static final int FREQ_END = 12000;
    
      private static final int MSG_PLAY_START   = 120;
      private static final int MSG_PLAY_END   = 130;
    
      private static final byte [] BITS = new byte [] {
          (byte)0x80, (byte)0x40, (byte)0x20, (byte)0x10,
          (byte)0x08, (byte)0x04, (byte)0x02, (byte)0x01};
    
      private Handler mHandler;
      private AudioTrack mAudioTrack = null;
      private ToggleButton mButton01;
      private EditText mEditText01;
      private short mPlayBuf1[];
      private short mPlayBuf2[];
      private short mPlayBuf3[];
      private String mText;
    
      // サイン波データを生成
      private void createSineWave(short[] buf, int freq, int amplitude, boolean doClear) {
        if (doClear) {
          Arrays.fill(buf, (short) 0);
        }
        for (int i = 0; i < ELMS_1SEC; i++) {
          float currentSec = i * SEC_PER_SAMPLEPOINT; // 現在位置の経過秒数
          double val = amplitude * Math.sin(2.0 * Math.PI * freq * currentSec);
          buf[i] += (short)val;
        }
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mButton01 = (ToggleButton)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mEditText01 = (EditText)findViewById(R.id.editText01);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mPlayBuf1 = new short[ELMS_1SEC];
        mPlayBuf2 = new short[ELMS_1SEC];
        mPlayBuf3 = new short[ELMS_1SEC];
        createSineWave(mPlayBuf1, FREQ_LOW, AMP, true); // Low
        createSineWave(mPlayBuf2, FREQ_HI,  AMP, true); // Hi
        createSineWave(mPlayBuf3, FREQ_END, AMP, true); // END
    
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        if (mButton01.isChecked()) {
          mText = mEditText01.getText().toString();
          if (mText.length() > 0) {
            new Thread(this).start();
          } else {
            mButton01.setChecked(false);
          }
        }
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_PLAY_START:
            Log.d(TAG, "MSG_PLAY_START");
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButton01.setChecked(false);
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_PLAY_START);
        byte[] strByte = null;
        try {
          strByte = mText.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
        }
        mAudioTrack.play();
        for (int i = 0; i < strByte.length; i++) {
          valueToWave(strByte[i]);
        }
        valueToWave((byte) 0x20); // ダミー
        mAudioTrack.write(mPlayBuf3, 0, ELMS_1SEC); // 終端
    
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    
      // 指定されたバイト値を音声信号に置き換えて再生する
      private void valueToWave(byte val) {
        for (int i = 0; i < BITS.length; i++) {
          // ビットごとに Hi, Low を出力
          mAudioTrack.write(((val & BITS[i]) != 0) ? mPlayBuf2 : mPlayBuf1, 0, ELMS_100MSEC);
        }
      }
    }
    
    

受信側: sonic06

  • MainActivity.java
    
    /**
     *
     * sonic06
     *
     * 端末のマイクから集音した信号をバイトデータに変換する
     * Lo -> 8000Hz, Hi -> 10000Hz
     * FFT 処理に JTransforms ライブラリを利用
     * 対向の送信プログラムは sonic05
     *
     */
    
    package jp.klab.sonic06;
    
    
    import org.jtransforms.fft.DoubleFFT_1D;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    import java.io.UnsupportedEncodingException;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final short THRESHOLD_SILENCE = 0x00ff;
      private static final int FREQ_LOW =  8000;
      private static final int FREQ_HI  = 10000;
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_DATA_RECV  = 130;
    
      private static final byte [] BITS = new byte [] {
          (byte)0x80, (byte)0x40, (byte)0x20, (byte)0x10,
          (byte)0x08, (byte)0x04, (byte)0x02, (byte)0x01};
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
    
      private Button mButton01;
      private TextView mTextView02;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mRecordBuf[];
      private short mTestBuf[];
      private DoubleFFT_1D mFFT;
      private double mFFTBuffer[];
      private int mFFTSize;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mTextView02 = (TextView)findViewById(R.id.textView02);
        mTextView02.setOnClickListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 録音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
    
        // FFT 処理用
        mTestBuf =  new short[SAMPLE_RATE/10]; // 100msec
        mFFTSize = mBufferSizeInShort;
        mFFT = new DoubleFFT_1D(mFFTSize);
        mFFTBuffer = new double[mFFTSize];
    
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        } else if (v == (View)mTextView02) {
          // 表示データをクリア
          mTextView02.setText("");
        }
        return;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setText("START");
            break;
          case MSG_DATA_RECV:
            //Log.d(TAG, "MSG_DATA_RECV");
            byte[] ch = new byte[] {(byte)msg.arg1};
            try {
              // 受信データを表示
              String s = new String(ch, "UTF-8");
              s = mTextView02.getText() + s;
              mTextView02.setText(s);
            } catch (UnsupportedEncodingException e) {
            }
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        int dataCount = 0;
        int bitCount = 0;
        byte val = 0;
        boolean bSilence = false;
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        while (mInRecording && !mStop) {
          // 音声データ読み込み
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          bSilence = true;
          for (int i = 0; i < mBufferSizeInShort; i++) {
            short s = mRecordBuf[i];
            if (s > THRESHOLD_SILENCE) {
              bSilence = false;
            }
          }
          if (bSilence) { // 静寂
            dataCount = bitCount = 0;
            continue;
          }
          int copyLength = 0;
          // データを mTestBuf へ順次アペンド
          if (dataCount < mTestBuf.length) {
            // mTestBuf の残領域に応じてコピーするサイズを決定
            int remain = mTestBuf.length - dataCount;
            if (remain > mBufferSizeInShort) {
              copyLength = mBufferSizeInShort;
            } else {
              copyLength = remain;
            }
            System.arraycopy(mRecordBuf, 0, mTestBuf, dataCount, copyLength);
            dataCount += copyLength;
          }
          if (dataCount >= mTestBuf.length) {
            byte ret = doFFT(mTestBuf);
            //Log.d(TAG, "ret=" + ret);
            if (ret == -1) { // FREQ_LOW, FREQ_HI 以外の周波数の場合
              dataCount = bitCount = 0;
              continue;
            } else {
              // バイトデータを順次構成
              if (bitCount == 0) {
                val = 0;
              }
              val |= (ret == 1) ? BITS[bitCount] : 0;
              if (bitCount < 7) {
                bitCount++;
              } else { // 1バイト分完了
                bitCount = 0;
                Message msg = new Message();
                msg.what = MSG_DATA_RECV;
                msg.arg1 = (int) val;
                mHandler.sendMessage(msg);
              }
            }
            dataCount = 0;
            // mRecordBuf の途中までを mTestBuf へコピーして FFT した場合は
            // mRecordBuf の残データを mTestBuf 先頭へコピーした上で継続
            if (copyLength < mBufferSizeInShort) {
              int startPos = copyLength;
              copyLength = mBufferSizeInShort - copyLength;
              System.arraycopy(mRecordBuf, startPos, mTestBuf, 0, copyLength);
              dataCount += copyLength;
            }
          }
        }
        // 録音終了
        mAudioRecord.stop();
        mHandler.sendEmptyMessage(MSG_RECORD_END);
      }
    
      private byte doFFT(short[] data) {
        for (int i = 0; i < mFFTSize; i++) {
          mFFTBuffer[i] = (double)data[i];
        }
        mFFT.realForward(mFFTBuffer);
    
        // 処理結果の複素数配列からピーク周波数成分の要素番号を得る
        double maxAmp = 0;
        int index = 0;
        for (int i = 0; i < mFFTSize/2; i++) {
          double a = mFFTBuffer[i*2]; // 実部
          double b = mFFTBuffer[i*2 + 1]; // 虚部
          // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
          double r = Math.sqrt(a*a + b*b);
          if (r > maxAmp) {
            maxAmp = r;
            index = i;
          }
        }
        // ピーク周波数を求める
        int freq = index * SAMPLE_RATE / mFFTSize;
        return (byte)((freq == FREQ_LOW) ? 0 : (freq == FREQ_HI) ? 1 : -1);
      }
    }
    
    

周波数切り替え箇所の波形を見る

信号中の Lo(8000Hz)から Hi(10000Hz)、Hi から Lo への切り替え部分の波形の確認を行いました。

Lo, Hi ともに 10 で割り切れる周波数なので、先頭から 100ミリ秒後の位置での運動円上の Y 座標は 1秒後の位置と等しく 0 であり、そこに同じく Y=0 始まりの別の周期のサイン波を接合しても位相の不連続は発生しないはずです。 (ただし周期の違いが継ぎ目前後のカーブの形状の違いとして表れるため波形として完璧ではないでしょう)

以下は送信側アプリからの信号出力を PC に接続したマイクから取り込んだ Audacity のスクリーンショットです。

Lo -> Hi 部分のズーム

後続の Hi -> Lo 部分を同様にズーム

2. ビットデータを音声信号で送受信する 2

上の最初の試作ではまず音によるデータの送受信自体が可能であることを確認しましたが、動作が不安定で何より通信の遅さが目につきます。この点について考えてみます。

伝送効率改善の考え方

効率を改善するために伝達する情報の内容に手を加えることにしました。1バイト分のキャラクタデータをビットごとに 0,1 を表わすふたつの周波数の波の羅列で表現するのではなく、256種類の周波数を用いて直接 0x00 〜 0xFF を表現する形にすれば理論上伝送効率が 8倍向上するはずです。これを具体化してみることにします。

試作

処理のイメージ

以下の要領で実装を行いました。

  • 送信側はバイト値 0x00 〜 0xFF を 400Hz 〜 5500Hz の 20Hz 刻みの 256 パターンの周波数から成る音声信号に変調する
  • 受信側は「(検知したピーク周波数 - 400)/ 20」の平易な計算で元のバイト値へ復号する
  • アプリの UI は最初の試作と同じ

動作の様子

動画: 1分  ※音量注意!

ソースコード

送信側: sonic07     GitHub

  • MainActivity.java
    /**
     * sonic07
     *
     * 文字列をサイン波信号に置き換えて出力する
     * 伝送効率改善版
     * 対向の受信プログラムは sonic08
     *
     */
    
    package jp.klab.sonic07;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.EditText;
    import android.widget.ToggleButton;
    
    import java.io.UnsupportedEncodingException;
    import java.util.Arrays;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final float SEC_PER_SAMPLEPOINT = 1.0f / SAMPLE_RATE;
      private static final int AMP = 4000;
      private static final int FREQ_BASE = 400;
      private static final int FREQ_STEP = 20;
      private static final int FREQ_KEY = 300;
      private static final int ELMS_1SEC = SAMPLE_RATE;
      private static final int ELMS_100MSEC = SAMPLE_RATE/10;
      private static final int ELMS_MAX = 256;
    
      private static final int MSG_PLAY_START   = 120;
      private static final int MSG_PLAY_END   = 130;
    
      private Handler mHandler;
      private AudioTrack mAudioTrack = null;
      private ToggleButton mButton01;
      private EditText mEditText01;
      private short mPlayBuf[] = new short[SAMPLE_RATE];
      private short mSignals[][] = new short[ELMS_MAX][SAMPLE_RATE/10];
      private String mText;
    
      // サイン波データを生成
      private void createSineWave(short[] buf, int freq, int amplitude, boolean doClear) {
        if (doClear) {
          Arrays.fill(buf, (short) 0);
        }
        for (int i = 0; i < buf.length; i++) {
          float currentSec = i * SEC_PER_SAMPLEPOINT; // 現在位置の経過秒数
          double val = amplitude * Math.sin(2.0 * Math.PI * freq * currentSec);
          buf[i] += (short)val;
        }
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mButton01 = (ToggleButton)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mEditText01 = (EditText)findViewById(R.id.editText01);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        // 先頭・終端の目印用信号データ
        createSineWave(mPlayBuf, FREQ_KEY, AMP, true);
    
        // 256種類の信号データを生成
        for (int i = 0; i < ELMS_MAX; i++) {
          createSineWave(mSignals[i], (short) (FREQ_BASE + FREQ_STEP*i), AMP, true);
        }
    
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        if (mButton01.isChecked()) {
          mText = mEditText01.getText().toString();
          if (mText.length() > 0) {
            new Thread(this).start();
          } else {
            mButton01.setChecked(false);
          }
        }
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_PLAY_START:
            Log.d(TAG, "MSG_PLAY_START");
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButton01.setChecked(false);
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_PLAY_START);
        byte[] strByte = null;
        try {
          strByte = mText.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
        }
        mAudioTrack.play();
        mAudioTrack.write(mPlayBuf, 0, ELMS_1SEC); // 開始
        for (int i = 0; i < strByte.length; i++) {
          valueToWave(strByte[i]);
        }
        mAudioTrack.write(mPlayBuf, 0, ELMS_1SEC); // 終端
    
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    
      // 指定されたバイト値を音声信号に置き換えて再生する
      private void valueToWave(byte val) {
        mAudioTrack.write(mSignals[val], 0, ELMS_100MSEC);
      }
    }
    
    
    

受信側: sonic08     GitHub

  • MainActivity.java
    
    /**
     * sonic08
     *
     * 端末のマイクから集音した信号をバイトデータに変換する
     * 伝送効率改善版
     * FFT 処理に JTransforms ライブラリを利用
     * 対向の送信プログラムは sonic07
     *
     */
    
    package jp.klab.sonic08;
    
    import org.jtransforms.fft.DoubleFFT_1D;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    import java.io.UnsupportedEncodingException;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final short THRESHOLD_SILENCE = 0x00ff;
      private static final int FREQ_BASE = 400;
      private static final int FREQ_STEP = 20;
      private static final int FREQ_MAX = 400 + 255 * 20;
      private static final int UNITSIZE = SAMPLE_RATE/10; // 100msec分
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_DATA_RECV  = 120;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
    
      private Button mButton01;
      private TextView mTextView02;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mRecordBuf[];
      private short mTestBuf[];
      private DoubleFFT_1D mFFT;
      private double mFFTBuffer[];
      private int mFFTSize;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mTextView02 = (TextView)findViewById(R.id.textView02);
        mTextView02.setOnClickListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 集音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
    
        // FFT 処理用
        mTestBuf =  new short[UNITSIZE];
        mFFTSize = UNITSIZE;
        mFFT = new DoubleFFT_1D(mFFTSize);
        mFFTBuffer = new double[mFFTSize];
    
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        } else if (v == (View)mTextView02) {
          // 表示データをクリア
          mTextView02.setText("");
        }
        return;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setText("START");
            break;
          case MSG_DATA_RECV:
            //Log.d(TAG, "MSG_DATA_RECV");
            byte[] ch = new byte[] {(byte)msg.arg1};
            try {
              // 受信データを表示
              String s = new String(ch, "UTF-8");
              s = mTextView02.getText() + s;
              mTextView02.setText(s);
            } catch (UnsupportedEncodingException e) {
            }
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        int dataCount = 0;
        boolean bSilence = false;
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        while (mInRecording && !mStop) {
          // 音声データ読み込み
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          bSilence = true;
          for (int i = 0; i < mBufferSizeInShort; i++) {
            short s = mRecordBuf[i];
            if (s > THRESHOLD_SILENCE) {
              bSilence = false;
            }
          }
          if (bSilence) { // 静寂
            dataCount = 0;
            continue;
          }
          int copyLength = 0;
          // データを mTestBuf へ順次アペンド
          if (dataCount < mTestBuf.length) {
            // mTestBuf の残領域に応じてコピーするサイズを決定
            int remain = mTestBuf.length - dataCount;
            if (remain > mBufferSizeInShort) {
              copyLength = mBufferSizeInShort;
            } else {
              copyLength = remain;
            }
            System.arraycopy(mRecordBuf, 0, mTestBuf, dataCount, copyLength);
            dataCount += copyLength;
          }
          if (dataCount >= mTestBuf.length) {
            // 100ms 分溜まったら FFT にかける
            int freq = doFFT(mTestBuf);
            // 待ってた範囲の周波数かチェック
            if (freq >= FREQ_BASE && freq <= FREQ_MAX) {
              int val = (int) ((freq - FREQ_BASE) / FREQ_STEP);
              if (val >= 0 && val <= 255) {
                Message msg = new Message();
                msg.what = MSG_DATA_RECV;
                msg.arg1 = val;
                mHandler.sendMessage(msg);
              } else {
                freq = -1;
              }
            } else {
              freq = -1;
            }
            dataCount = 0;
            if (freq == -1) {
              continue;
            }
            // mRecordBuf の途中までを mTestBuf へコピーして FFT した場合は
            // mRecordBuf の残データを mTestBuf 先頭へコピーした上で継続
            if (copyLength < mBufferSizeInShort) {
              int startPos = copyLength;
              copyLength = mBufferSizeInShort - copyLength;
              System.arraycopy(mRecordBuf, startPos, mTestBuf, 0, copyLength);
              dataCount += copyLength;
            }
          }
        }
        // 集音終了
        mAudioRecord.stop();
        mHandler.sendEmptyMessage(MSG_RECORD_END);
      }
    
      private int doFFT(short[] data) {
        for (int i = 0; i < mFFTSize; i++) {
          mFFTBuffer[i] = (double)data[i];
        }
        // FFT 実行
        mFFT.realForward(mFFTBuffer);
    
        // 処理結果の複素数配列からピーク周波数成分の要素番号を得る
        double maxAmp = 0;
        int index = 0;
        for (int i = 0; i < mFFTSize/2; i++) {
          double a = mFFTBuffer[i*2]; // 実部
          double b = mFFTBuffer[i*2 + 1]; // 虚部
          // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
          double r = Math.sqrt(a*a + b*b);
          if (r > maxAmp) {
            maxAmp = r;
            index = i;
          }
        }
        // ピーク周波数を求める
        int freq = index * SAMPLE_RATE / mFFTSize;
        //Log.d(TAG, "peak=" + freq + "Hz");
        return freq;
      }
    }
    
    

3. ビットデータを音声信号で送受信する 3

伝送効率の改善に続き、次の要素に目を向けてみます。

  • 誤り発生への対処
  • 超音波の利用

誤り発生への対処について

他の通信方法と同様に、音波による通信においても送受信の際に何らかの理由でデータに誤りが発生する可能性があります。この問題への対処について考えてみます。

まず誤りを検出可能であること、さらに、自動的にデータを訂正することができれば理想的でしょう。しかしながら、誤り訂正を実現するためには何らかの形でデータの冗長化が必須となり必然的に伝送効率とのトレードオフとなります。冗長化されたより大きなデータを授受することで自動訂正実現の可能性向上を選択すべきか?あるいは、誤り有無判定のみを可能とするスリムなデータを短時間で授受することを旨とするか?

たとえば、プライベートな環境での音波通信の応用を想定すると、エラー発生に気づいた利用者が自発的に送受信を再試行するのはごく普通の自然な流れです。この場面では訂正機能よりもすみやかに通信が行われることのほうが求められるでしょう。また、パブリックな空間でのビーコン系サービスにおいては送り手が同一の情報を所定の期間何度も繰り返し発信する(それもまた単方向通信であることに起因)ことが一般的なので受け手には情報取得に成功するまで何度もチャンスが提供されます。

このように考えると、ここでは誤り訂正のための機構は現在の話題に不可欠の要素ではなく、むしろ誤りの検出までが重要であることが見えてきます。

超音波の利用について

ここまでの試作では開発上の便宜から可聴音域の信号で通信を行ってきました。

音を利用する 1」冒頭の「はじめに」の項でも触れたように、音声信号を利用する際の音域の選定は環境や使途とのかねあいによって単に適材適所で判断されるべきはずのものですが、そのこととはあまり関係なく洋の東西を問わず「超音波」「Ultrasonic」は人気がありますね。そろそろこのあたりで従来の可聴音に加え超音波も扱ってみることにします。

試作

処理のイメージ

以下の要領で実装を行いました。

可聴音モードと超音波モード

「超音波モード」を追加。

  • 送信側は所定のバイト値 0x00 〜 0xFF を以下の周波数域で構成される音声信号に変調する
    (前項のふたつめの試作では 20Hz刻みとしたが 10Hz刻みでも性能上の支障が見られないため帯域幅を節約)
    • 可聴音モード:500Hz 〜 3050Hz(10Hz 刻み)
    • 超音波モード:14000Hz 〜 16550Hz(10Hz 刻み)

先端符丁と終端符丁

発信する音声信号の先頭と終端に目印として上図の範囲に含まれない所定の周波数の信号を 1秒間付与する。

  • 先端符丁:各モードで使用する音域の先頭周波数 − 80Hz(=420Hz,13920Hz)
  • 終端符丁:各モードで使用する音域の先頭周波数 − 100Hz(=400Hz, 13900Hz)

誤り検出

次の内容で誤り検出機構を追加。

  • 送信側は先端符丁とデータ本体の間にデータの 32ビット CRC 計算結果(java.util.zip.CRC32 使用)を 4 * 100ミリ秒間発信する
  • 受信側は先端符丁を検知すると上記 CRC 値を取得・復号して保持。終端符丁検出時に復号ずみデータ本体の CRC を計算し両者を比較することで誤りの可能性の有無を判定する
    • データ本体の受け渡しに失敗したケースのみならず CRC 値の受け渡しに失敗のケースもありえるが、現時点では CRC 値情報授受の冗長化は考えず一律に「エラー」とみなす

UI

  • 左:送信側 右:受信側
    両者とも「Ultrasonic」スイッチで超音波モードへ移行

動作の様子

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

ソースコード

送信側: sonic09     GitHub

  • MainActivity.java
    
    /**
     * sonic09
     *
     * 文字列をサイン波信号に置き換えて出力する
     * 32ビットCRC付与, 超音波モード追加
     * 対向の受信プログラムは sonic10
     *
     */
    
    package jp.klab.sonic09;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.CompoundButton;
    import android.widget.EditText;
    import android.widget.Switch;
    import android.widget.ToggleButton;
    
    import java.io.UnsupportedEncodingException;
    import java.util.Arrays;
    import java.util.zip.CRC32;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener,
        Switch.OnCheckedChangeListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final float SEC_PER_SAMPLEPOINT = 1.0f / SAMPLE_RATE;
      private static final int FREQ_BASE_LOW = 500;
      private static final int FREQ_BASE_HIGH = 14000;
      private static final int AMP_SMALL = 5000;
      private static final int AMP_LARGE = 28000;
    
      private static final int ELMS_1SEC = SAMPLE_RATE;
      private static final int ELMS_UNITSEC = SAMPLE_RATE/10;
      private static final int ELMS_MAX = 256;
    
      private static final int MSG_PLAY_START   = 120;
      private static final int MSG_PLAY_END   = 130;
    
      private int FREQ_STEP = 10;
      private int AMP;
      private int FREQ_BASE;
      private int FREQ_OUT;
      private int FREQ_IN;
    
      private Handler mHandler;
      private AudioTrack mAudioTrack = null;
      private ToggleButton mButton01;
      private EditText mEditText01;
      private Switch mSwitch01;
    
      private short mSigIn[] = new short[SAMPLE_RATE];
      private short mSigOut[] = new short[SAMPLE_RATE];
      private short mSignals[][] = new short[ELMS_MAX][ELMS_UNITSEC];
      private String mText;
    
      // サイン波データを生成
      private void createSineWave(short[] buf, int freq, int amplitude, boolean doClear) {
        if (doClear) {
          Arrays.fill(buf, (short) 0);
        }
        for (int i = 0; i < buf.length; i++) {
          float currentSec = i * SEC_PER_SAMPLEPOINT; // 現在位置の経過秒数
          double val = amplitude * Math.sin(2.0 * Math.PI * freq * currentSec);
          buf[i] += (short)val;
        }
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mButton01 = (ToggleButton)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mEditText01 = (EditText)findViewById(R.id.editText01);
        mSwitch01 = (Switch)findViewById(R.id.switch01);
        mSwitch01.setOnCheckedChangeListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
        setParams(false);
    
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        if (mButton01.isChecked()) {
          mText = mEditText01.getText().toString();
          if (mText.length() > 0) {
            new Thread(this).start();
          } else {
            mButton01.setChecked(false);
          }
        }
      }
      @Override
      public void onCheckedChanged(CompoundButton b, boolean isChecked) {
        setParams(isChecked);
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_PLAY_START:
            Log.d(TAG, "MSG_PLAY_START");
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButton01.setChecked(false);
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_PLAY_START);
        byte[] strByte = null;
        try {
          strByte = mText.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
        }
        mAudioTrack.play();
        mAudioTrack.write(mSigIn, 0, ELMS_1SEC); // 先端符丁
        // データ本体の CRC32 を計算して発信
        CRC32 crc = new CRC32();
        crc.reset();
        crc.update(strByte, 0, strByte.length);
        long crcVal = crc.getValue();
        //Log.d(TAG, "crc=" + Long.toHexString(crcVal));
        byte crcData[] = new byte[4];
        for (int i = 0; i < 4; i++) {
          crcData[i] = (byte)(crcVal >> (24-i*8));
          valueToWave((short) (crcData[i] & 0xFF));
        }
        // データ本体
        for (int i = 0; i < strByte.length; i++) {
          valueToWave(strByte[i]);
        }
        mAudioTrack.write(mSigOut, 0, ELMS_1SEC); // 終端符丁
    
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    
      // 指定されたバイト値を音声信号に置き換えて再生する
      private void valueToWave(short val) {
        //Log.d(TAG, "val=" + val);
        mAudioTrack.write(mSignals[val], 0, ELMS_UNITSEC);
      }
    
      private void setParams(boolean useUltrasonic) {
        AMP = (useUltrasonic) ? AMP_LARGE : AMP_SMALL;
        FREQ_BASE = (useUltrasonic) ? FREQ_BASE_HIGH : FREQ_BASE_LOW;
        FREQ_OUT = FREQ_BASE - 100;
        FREQ_IN = FREQ_OUT + 20;
        // 先端・終端符丁の信号データを生成
        createSineWave(mSigIn, FREQ_IN, AMP, true);
        createSineWave(mSigOut, FREQ_OUT, AMP, true);
        // 256種類の信号データを生成
        for (int i = 0; i < ELMS_MAX; i++) {
          createSineWave(mSignals[i], (short) (FREQ_BASE + FREQ_STEP * i), AMP, true);
        }
    
      }
    }
    
    

受信側: sonic10     GitHub

  • MainActivity.java
    
    /**
     * sonic10
     *
     * 端末のマイクから集音した信号をバイトデータに変換する
     * CRC32による誤り検出, 超音波モードへ対応
     * FFT 処理に JTransforms ライブラリを利用
     * 対向の送信プログラムは sonic09
     *
     */
    
    package jp.klab.sonic10;
    
    import org.jtransforms.fft.DoubleFFT_1D;
    import android.graphics.Color;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.CompoundButton;
    import android.widget.Switch;
    import android.widget.TextView;
    
    import java.io.UnsupportedEncodingException;
    import java.util.ArrayList;
    import java.util.zip.CRC32;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener,
          Switch.OnCheckedChangeListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final short THRESHOLD_SILENCE = 0x00ff;
      private static final int FREQ_BASE_LOW = 500;
      private static final int FREQ_BASE_HIGH = 14000;
      private static final int FREQ_STEP = 10;
      private static final int UNITSIZE = SAMPLE_RATE/10; // 100msec分
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_DATA_RECV  = 120;
      private static final int MSG_RECV_OK    = 200;
      private static final int MSG_RECV_NG    = 210;
    
      private int FREQ_BASE = FREQ_BASE_LOW;
      private int FREQ_OUT = FREQ_BASE - 100;
      private int FREQ_IN = FREQ_OUT + 20;
      private int FREQ_MAX = FREQ_BASE + FREQ_STEP * 255;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
    
      private Button mButton01;
      private TextView mTextView02;
      private TextView mTextView03;
      private Switch mSwitch01;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mRecordBuf[];
      private short mTestBuf[];
      private DoubleFFT_1D mFFT;
      private double mFFTBuffer[];
      private int mFFTSize;
      private byte mValueCount = -1;
      private long mCrc32Val = 0;
      private String mRecvStr;
      private ArrayList<Byte> mDataArrayList = new ArrayList<Byte>();
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mTextView02 = (TextView)findViewById(R.id.textView02);
        mTextView02.setOnClickListener(this);
        mTextView03 = (TextView)findViewById(R.id.textView03);
        mTextView03.setTextColor(Color.RED);
        mSwitch01 = (Switch)findViewById(R.id.switch01);
        mSwitch01.setOnCheckedChangeListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 集音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
    
        // FFT 処理用
        mTestBuf =  new short[UNITSIZE];
        mFFTSize = UNITSIZE;
        mFFT = new DoubleFFT_1D(mFFTSize);
        mFFTBuffer = new double[mFFTSize];
    
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        } else if (v == (View)mTextView02) {
          // 表示データをクリア
          mTextView02.setText("");
          mTextView03.setText("");
        }
        return;
      }
      @Override
      public void onCheckedChanged(CompoundButton b, boolean isChecked) {
        if (b == (CompoundButton)mSwitch01) {
          FREQ_BASE = (isChecked) ? FREQ_BASE_HIGH : FREQ_BASE_LOW;
          FREQ_OUT = FREQ_BASE - 100;
          FREQ_IN = FREQ_OUT + 20;
          FREQ_MAX = FREQ_BASE + FREQ_STEP * 255;
        }
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            mSwitch01.setEnabled(false);
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setText("START");
            mSwitch01.setEnabled(true);
            break;
          case MSG_DATA_RECV:
            //Log.d(TAG, "MSG_DATA_RECV");
            byte[] ch = new byte[] {(byte)msg.arg1};
            try {
              // 受信データを表示
              String s = new String(ch, "UTF-8");
              s = mTextView02.getText() + s;
              mTextView02.setText(s);
            } catch (UnsupportedEncodingException e) {
            }
            mTextView03.setText("");
            break;
          case MSG_RECV_OK:
            mTextView03.setText("OK!");
            break;
          case MSG_RECV_NG:
            mTextView03.setText("NG!");
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        int dataCount = 0;
        boolean bSilence = false;
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        while (mInRecording && !mStop) {
          // 音声データ読み込み
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          bSilence = true;
          for (int i = 0; i < mBufferSizeInShort; i++) {
            short s = mRecordBuf[i];
            if (s > THRESHOLD_SILENCE) {
              bSilence = false;
            }
          }
          if (bSilence) { // 静寂
            dataCount = 0;
            continue;
          }
          int copyLength = 0;
          // データを mTestBuf へ順次アペンド
          if (dataCount < UNITSIZE) {//mTestBuf.length) {
            // mTestBuf の残領域に応じてコピーするサイズを決定
            int remain = UNITSIZE - dataCount;
            if (remain > mBufferSizeInShort) {
              copyLength = mBufferSizeInShort;
            } else {
              copyLength = remain;
            }
            System.arraycopy(mRecordBuf, 0, mTestBuf, dataCount, copyLength);
            dataCount += copyLength;
          }
          if (dataCount >= UNITSIZE) {//mTestBuf.length) {
            // 100ms 分溜まったら FFT にかける
            int freq = doFFT(mTestBuf);
            if (mValueCount < 0) {
              // データ終了
              if (freq == FREQ_OUT && mCrc32Val != 0) {
                byte check [] = new byte[mDataArrayList.size()];
                for (int i = 0; i < check.length; i++) {
                  check[i] = mDataArrayList.get(i);
                }
                CRC32 crc = new CRC32();
                crc.reset();
                crc.update(check, 0, check.length);
                long crcVal = crc.getValue();
                Log.d(TAG, "crc check=" +  Long.toHexString(crcVal));
                if (crcVal == mCrc32Val) {
                  mHandler.sendEmptyMessage(MSG_RECV_OK);
                } else {
                  mHandler.sendEmptyMessage(MSG_RECV_NG);
                }
                mCrc32Val = 0;
              }
            }
            // 待ってた範囲の周波数かチェック
            if (freq >= FREQ_BASE && freq <= FREQ_MAX) {
              int val = (int) ((freq - FREQ_BASE) / FREQ_STEP);
              if (val >= 0 && val <= 255) {
                if (mValueCount > 4) {
                  mDataArrayList.add((byte)val);
                  Message msg = new Message();
                  msg.what = MSG_DATA_RECV;
                  msg.arg1 = val;
                  mHandler.sendMessage(msg);
                }
              } else {
                freq = -1;
              }
            } else {
              freq = -1;
            }
            dataCount = 0;
            if (freq == -1) {
              continue;
            }
            // mRecordBuf の途中までを mTestBuf へコピーして FFT した場合は
            // mRecordBuf の残データを mTestBuf 先頭へコピーした上で継続
            if (copyLength < mBufferSizeInShort) {
              int startPos = copyLength;
              copyLength = mBufferSizeInShort - copyLength;
              System.arraycopy(mRecordBuf, startPos, mTestBuf, 0, copyLength);
              dataCount += copyLength;
            }
          }
        }
        // 集音終了
        mAudioRecord.stop();
        mHandler.sendEmptyMessage(MSG_RECORD_END);
      }
    
      private int doFFT(short[] data) {
        for (int i = 0; i < mFFTSize; i++) {
          mFFTBuffer[i] = (double)data[i];
        }
        // FFT 実行
        mFFT.realForward(mFFTBuffer);
    
        // 処理結果の複素数配列からピーク周波数成分の要素番号を得る
        double maxAmp = 0;
        int index = 0;
        for (int i = 0; i < mFFTSize/2; i++) {
          double a = mFFTBuffer[i*2]; // 実部
          double b = mFFTBuffer[i*2 + 1]; // 虚部
          // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
          double r = Math.sqrt(a*a + b*b);
          if (r > maxAmp) {
            maxAmp = r;
            index = i;
          }
        }
        // ピーク周波数を求める
        int freq = index * SAMPLE_RATE / mFFTSize;
        byte val = (byte)((freq-FREQ_BASE)/FREQ_STEP);
    
        if (freq == FREQ_IN) { // 先端符丁
          mValueCount = 0;
          mCrc32Val = 0;
          if (!mDataArrayList.isEmpty()) {
            mDataArrayList.clear();
          }
          return freq;
        } else if (freq == FREQ_OUT) { // 終端符丁
          mValueCount = -1;
          return freq;
        }
    
        // 先端符丁直後の 4バイトは32ビットCRC
        if (mValueCount >= 0 &&  mValueCount < 4) {
          mCrc32Val |=  (val & 0xFF);
          if (mValueCount != 3) {
            mCrc32Val <<= 8;
          } else {
            Log.d(TAG, "mCrc32Val=" + Long.toHexString(mCrc32Val));
          }
        }
        mValueCount++;
        return freq;
      }
    }
    
    

4. ビットデータを音声信号で送受信する 4

ここまでの試作では単方向の通信のみを扱ってきました。これは以下の理由によるものです。

  1. 集音・発音の両機能を混在させると自機の発した信号音による混乱が起こり得る
  2. 伝送が基本的に低速であるため機器間で相互に通信を行うなら双方の拘束時間が長くなることになり実用性を想定しにくい
  3. 双方向通信に伴いプロトコルが煩雑化することよりも単方向通信であることを活かせる用途を選ぶべき

これらはおそらく考え方として間違っていないように思いますが、一方で、二台の機器が相互に音で情報をやりとりする光景を想像すると理由のない好奇心をくすぐられます。そこで、実験の一環として最後にデバイス間の双方向通信を試すことにしました。

双方向通信を考える

双方向通信の方法として以下の内容を想定しました。

試作

処理のイメージ

以下の要領で実装を行いました。

「しりとり」を題材とする

  • 機器間で自律的にある程度意味のある応酬を行わせるために英単語のしりとりを題材とした
    ability -> yellow -> warn -> national .... の要領
  • 機器間の応酬は自動化し最初の単語を発信するために初回のみ UI を操作する形とした

集音・発音の切り替え

  • 集音状態での待機中に先端符丁の信号音を検知したらデータ到着とみなし後続処理を開始。その後終端符丁を検知したら所定の時間間隔をおいて発信を行う。発信が完了したら集音状態での待機に戻る

再送要求

  • 受信・復号したデータの内容に誤りを検出した場合、「?」文字を発信することで相手に再送を要求する

UI

  • タイトル部分をタップすると最初の単語を発信。あとはエンドレス

動作の様子

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

ソースコード

sonic11     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic11
     *
     * サイン波音声信号による半二重式での双方向通信のしくみの試作
     * 二台の機器間で「しりとり」を行う
     * FFT 処理に JTransforms ライブラリを利用
     *
     */
    
    package jp.klab.sonic11;
    
    import org.jtransforms.fft.DoubleFFT_1D;
    import android.graphics.Color;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.CompoundButton;
    import android.widget.Switch;
    import android.widget.TextView;
    
    import java.io.UnsupportedEncodingException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.zip.CRC32;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener,
          Switch.OnCheckedChangeListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final float SEC_PER_SAMPLEPOINT = 1.0f / SAMPLE_RATE;
      private static final short THRESHOLD_SILENCE = 0x00ff;
      private static final int THRESHOLD_COUNT_SILENCE = 25; // 無音区間判定用
      private static final int FREQ_BASE_LOW = 500;
      private static final int FREQ_BASE_HIGH = 14000;
      private static final int AMP_SMALL = 28000;
      private static final int AMP_LARGE = 28000;
    
      private static final int ELMS_1SEC = SAMPLE_RATE;
      private static final int ELMS_UNITSEC = SAMPLE_RATE/10;
      private static final int ELMS_MAX = 256;
      private static final int FREQ_STEP = 10;
      private static final int UNITSIZE = SAMPLE_RATE/10; // 100msec分
    
      private static final int MSG_RECORD_START  = 100;
      private static final int MSG_RECORD_END    = 110;
      private static final int MSG_DATA_RECV     = 130;
      private static final int MSG_DATA_SEND_START = 140;
      private static final int MSG_RECV_OK     = 200;
      private static final int MSG_RECV_NG     = 210;
    
      private int AMP;
      private int FREQ_BASE;
      private int FREQ_OUT;
      private int FREQ_IN;
      private int FREQ_MAX = FREQ_BASE + FREQ_STEP * 255;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
      private AudioTrack mAudioTrack = null;
    
      private Button mButton01;
      private TextView mTextView01;
      private TextView mTextView02;
      private TextView mTextView03;
      private TextView mTextView04;
      private Switch mSwitch01;
    
      private short mSigIn[] = new short[SAMPLE_RATE];
      private short mSigOut[] = new short[SAMPLE_RATE];
      private short mSignals[][] = new short[ELMS_MAX][SAMPLE_RATE/10];
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mRecordBuf[];
      private short mTestBuf[];
      private DoubleFFT_1D mFFT;
      private double mFFTBuffer[];
      private int mFFTSize;
      private byte mValueCount = -1;
      private long mCrc32Val = 0;
      private ArrayList<Byte> mDataArrayList = new ArrayList<Byte>();
      private int mLastFreq;
      private String mRecvWord = "";
      private String mSendWord = "";
      private boolean mRecvOK;
    
      // サイン波データを生成
      private void createSineWave(short[] buf, int freq, int amplitude, boolean doClear) {
        if (doClear) {
          Arrays.fill(buf, (short) 0);
        }
        for (int i = 0; i < buf.length; i++) {
          float currentSec = i * SEC_PER_SAMPLEPOINT; // 現在位置の経過秒数
          double val = amplitude * Math.sin(2.0 * Math.PI * freq * currentSec);
          buf[i] += (short)val;
        }
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mTextView01 = (TextView)findViewById(R.id.textView01);
        mTextView01.setOnClickListener(this);
        mTextView02 = (TextView)findViewById(R.id.textView02);
        mTextView02.setOnClickListener(this);
        mTextView03 = (TextView)findViewById(R.id.textView03);
        mTextView03.setTextColor(Color.RED);
        mTextView04 = (TextView)findViewById(R.id.textView04);
        mSwitch01 = (Switch)findViewById(R.id.switch01);
        mSwitch01.setOnCheckedChangeListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 集音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
    
        // FFT 処理用
        mTestBuf = new short[UNITSIZE];
        mFFTSize = UNITSIZE;
        mFFT = new DoubleFFT_1D(mFFTSize);
        mFFTBuffer = new double[mFFTSize];
    
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
    
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
            SAMPLE_RATE,
            AudioFormat.CHANNEL_OUT_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSizeInBytes,
            AudioTrack.MODE_STREAM);
    
        setParams(false);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        } else if (v == (View)mTextView01) {
          // しりとりの開始・再開
          // すでに集音中なら自分で自分の音を拾わないように
          // スレッドを一旦終了
          boolean running = mInRecording;
          if (mInRecording) {
            mStop = true;
            try {
              while (mInRecording) {
                Thread.sleep(100);
              }
            } catch (InterruptedException e) {
            }
          }
          // ランダムに選んだ英単語を音で発信
          mSendWord = words.getWord();
          doSendWord(mSendWord);
          if (running) {
            // 集音スレッド再稼働
            mInRecording = true;
            mStop = false;
            new Thread(this).start();
          }
        } else if (v == (View)mTextView02) {
          mTextView02.setText("");
          mTextView03.setText("");
        }
        return;
      }
    
      @Override
      public void onCheckedChanged(CompoundButton b, boolean isChecked) {
        if (b == (CompoundButton)mSwitch01) {
          setParams(isChecked);
        }
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            mSwitch01.setEnabled(false);
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setText("START");
            mSwitch01.setEnabled(true);
            break;
          case MSG_DATA_RECV:
            //Log.d(TAG, "MSG_DATA_RECV");
            if (mRecvWord.length() <= 0) {
              mTextView02.setText("RECV: ");
            }
            byte[] ch = new byte[] {(byte)msg.arg1};
            try {
              // 受信データを表示
              String s = new String(ch, "UTF-8");
              mRecvWord += s;
              s = mTextView02.getText() + s;
              mTextView02.setText(s);
            } catch (UnsupportedEncodingException e) {
            }
            mTextView03.setText("");
            break;
          case MSG_DATA_SEND_START:
            mTextView04.setText("SEND: " + mSendWord);
            break;
          case MSG_RECV_OK:
            mTextView03.setText("OK!");
            break;
          case MSG_RECV_NG:
            mTextView03.setText("NG!");
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        int dataCount = 0;
        boolean bSilence = false;
        long countSilence = 0;
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        while (mInRecording && !mStop) {
          // 音声信号読み込み
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          bSilence = true;
          for (int i = 0; i < mBufferSizeInShort; i++) {
            short s = mRecordBuf[i];
            if (s > THRESHOLD_SILENCE) {
              bSilence = false;
              break;
            }
          }
          if (bSilence) { // 静寂
            dataCount = 0;
            countSilence++;
          } else {
            countSilence = 0;
          }
          // 単語を受信ずみかつ一定期間以上静寂が続いたら送信にまわる
          if (countSilence > THRESHOLD_COUNT_SILENCE &&
              mLastFreq == FREQ_OUT && mRecvWord.length() > 0) {
            if (mRecvOK) { // 直前の受信・復号結果が正常だった
              if (!mRecvWord.equals("?")) {
                // 受信した単語のしりとり語を取得
                mSendWord = words.getWord(mRecvWord);
              }
            } else {
              // 再送要求
              mSendWord = "?";
            }
            Log.d(TAG, "mSendWord=" + mSendWord);
            // 単語を発信
            doSendWord(mSendWord);
            continue;
          }
    
          int copyLength = 0;
          // データを mTestBuf へ順次アペンド
          if (dataCount < UNITSIZE) {
            // mTestBuf の残領域に応じてコピーするサイズを決定
            int remain = UNITSIZE - dataCount;
            if (remain > mBufferSizeInShort) {
              copyLength = mBufferSizeInShort;
            } else {
              copyLength = remain;
            }
            System.arraycopy(mRecordBuf, 0, mTestBuf, dataCount, copyLength);
            dataCount += copyLength;
          }
          if (dataCount >= UNITSIZE) {
            // 100ms 分溜まったら FFT にかける
            int freq = doFFT(mTestBuf);
            mLastFreq = freq;
            if (mValueCount < 0) {
              // データ終了
              if (freq == FREQ_OUT && mCrc32Val != 0) {
                byte check [] = new byte[mDataArrayList.size()];
                for (int i = 0; i < check.length; i++) {
                  check[i] = mDataArrayList.get(i);
                }
                //Log.d(TAG, "mRecvWord=" + mRecvWord);
                CRC32 crc = new CRC32();
                crc.reset();
                crc.update(check, 0, check.length);
                long crcVal = crc.getValue();
                //Log.d(TAG, "crc check=" +  Long.toHexString(crcVal));
                mRecvOK = false;
                if (crcVal == mCrc32Val) {
                  mRecvOK = true;
                  mHandler.sendEmptyMessage(MSG_RECV_OK);
                } else {
                  mHandler.sendEmptyMessage(MSG_RECV_NG);
                }
                mCrc32Val = 0;
              }
            }
            // 待ってた範囲の周波数かチェック
            if (freq >= FREQ_BASE && freq <= FREQ_MAX) {
              int val = (int) ((freq - FREQ_BASE) / FREQ_STEP);
              if (val >= 0 && val <= 255) {
                if (mValueCount > 4) {
                  mDataArrayList.add((byte)val);
                  Message msg = new Message();
                  msg.what = MSG_DATA_RECV;
                  msg.arg1 = val;
                  mHandler.sendMessage(msg);
                }
              } else {
                freq = -1;
              }
            } else {
              freq = -1;
            }
            dataCount = 0;
            if (freq == -1) {
              continue;
            }
            // mRecordBuf の途中までを mTestBuf へコピーして FFT した場合は
            // mRecordBuf の残データを mTestBuf 先頭へコピーした上で継続
            if (copyLength < mBufferSizeInShort) {
              int startPos = copyLength;
              copyLength = mBufferSizeInShort - copyLength;
              System.arraycopy(mRecordBuf, startPos, mTestBuf, 0, copyLength);
              dataCount += copyLength;
            }
          }
        }
        // 集音終了
        mAudioRecord.stop();
        mInRecording = false;
        mHandler.sendEmptyMessage(MSG_RECORD_END);
      }
    
      private int doFFT(short[] data) {
        for (int i = 0; i < mFFTSize; i++) {
          mFFTBuffer[i] = (double)data[i];
        }
        // FFT 実行
        mFFT.realForward(mFFTBuffer);
    
        // 処理結果の複素数配列からピーク周波数成分の要素番号を得る
        double maxAmp = 0;
        int index = 0;
        for (int i = 0; i < mFFTSize/2; i++) {
          double a = mFFTBuffer[i*2]; // 実部
          double b = mFFTBuffer[i*2 + 1]; // 虚部
          // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
          double r = Math.sqrt(a*a + b*b);
          if (r > maxAmp) {
            maxAmp = r;
            index = i;
          }
        }
        // ピーク周波数を求める
        int freq = index * SAMPLE_RATE / mFFTSize;
        byte val = (byte)((freq-FREQ_BASE)/FREQ_STEP);
    
        if (freq == FREQ_IN) { // 先端符丁
          mValueCount = 0;
          mCrc32Val = 0;
          if (!mDataArrayList.isEmpty()) {
            mDataArrayList.clear();
          }
          mRecvWord = "";
          return freq;
        } else if (freq == FREQ_OUT) { // 終端符丁
          mValueCount = -1;
          return freq;
        }
    
        // 先端符丁直後の 4バイトは32ビットCRC
        if (mValueCount >= 0 &&  mValueCount < 4) {
          mCrc32Val |=  (val & 0xFF);
          if (mValueCount != 3) {
            mCrc32Val <<= 8;
          } else {
            Log.d(TAG, "mCrc32Val=" + Long.toHexString(mCrc32Val));
          }
        }
        mValueCount++;
        return freq;
      }
    
      // 指定されたバイト値を音声信号に置き換えて再生する
      private void valueToWave(short val) {
        //Log.d(TAG, "val=" + val);
        mAudioTrack.write(mSignals[val], 0, ELMS_UNITSEC);
      }
    
      private void doSendWord(String str) {
        mSendWord = str;
        mHandler.sendEmptyMessage(MSG_DATA_SEND_START);
        Log.d(TAG, "mSendWord=" + mSendWord);
        byte[] strByte = null;
        try {
          strByte = mSendWord.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
        }
        mAudioTrack.play();
        mAudioTrack.write(mSigIn, 0, ELMS_1SEC); // 先端符丁
        // データ本体の CRC32 を計算して発信
        CRC32 crc = new CRC32();
        crc.reset();
        crc.update(strByte, 0, strByte.length);
        long crcVal = crc.getValue();
        //Log.d(TAG, "crc=" + Long.toHexString(crcVal));
        byte crcData[] = new byte[4];
        for (int i = 0; i < 4; i++) {
          crcData[i] = (byte)(crcVal >> (24-i*8));
          valueToWave((short) (crcData[i] & 0xFF));
        }
        // データ本体
        for (int i = 0; i < strByte.length; i++) {
          valueToWave(strByte[i]);
        }
        mAudioTrack.write(mSigOut, 0, ELMS_1SEC); // 終端符丁
        mAudioTrack.stop();
        mAudioTrack.flush();
        mLastFreq = -1;
        mRecvWord = "";
      }
    
      private void setParams(boolean useUltrasonic) {
        AMP = (useUltrasonic) ? AMP_LARGE : AMP_SMALL;
        FREQ_BASE = (useUltrasonic) ? FREQ_BASE_HIGH : FREQ_BASE_LOW;
        FREQ_OUT = FREQ_BASE - 100;
        FREQ_IN = FREQ_OUT + 20;
        // 先端・終端符丁の信号データを生成
        createSineWave(mSigIn, FREQ_IN, AMP, true);
        createSineWave(mSigOut, FREQ_OUT, AMP, true);
        // 256種類の信号データを生成
        for (int i = 0; i < ELMS_MAX; i++) {
          createSineWave(mSignals[i], (short) (FREQ_BASE + FREQ_STEP * i), AMP, true);
        }
        FREQ_MAX = FREQ_BASE + FREQ_STEP * 255;
      }
    }
    
    

(tanabe)
klab_gijutsu2 at 09:00
この記事のURLComments(0)TrackBack(0)Android | IoT
2017年06月02日

音を利用する 2 〜Android デバイスでの音波通信:準備編〜

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

この記事の内容

前回の記事では音を媒体とする機器間の情報通信が近年ふたたび注目されていることに触れ複数の具体的な事例を挙げました。近距離通信手段としての音波の利用には技術的好奇心をそそられますが、それをどのように形にすればよいのかは案外ピンと来ません。そこで、Android デバイスを題材に手元で行った音波通信の実験と試作の内容をこれから何回かに分けて紹介します。今回は準備編として実装に必要な要素の整理を行います。

準備 1. Andorid での音声入出力処理を覚える

プラットフォームにより開発環境や API に違いはあっても音を扱う処理の本質は変わりません。手元でターゲットとして Android を選んだのは以下の理由によるものです。

  • メジャーである
  • サウンド I/O を標準で利用できる(マイコン系と比べての利点)
  • ポータブルであるため応用が利きやすく IoT 方面との連携性も良い(PC と比べての利点)
  • 開発環境の制約ごとが相対的に少ない(他のスマホ系と比べての利点)

まず、今回使用した音声入出力 API の情報をリファレンスより引用します。特に難しい要素もなく分かりやすい内容でした。

入力:AudioRecord クラス

  • AudioRecord
    The AudioRecord class manages the audio resources for Java applications to record audio from the audio input hardware of the platform.
          :
    Upon creation, an AudioRecord object initializes its associated audio buffer that it will fill with the new audio data.
          :
    Data should be read from the audio hardware in chunks of sizes inferior to the total recording buffer size.
    • AudioRecord (constructor)
      AudioRecord (int audioSource,
            int sampleRateInHz,
            int channelConfig,
            int audioFormat,
            int bufferSizeInBytes)
            :
      Parameters
            :
      bufferSizeInBytes
      int: the total size (in bytes) of the buffer where audio data is written to during the recording. New audio data can be read from this buffer in smaller chunks than this size. See getMinBufferSize(int, int, int) to determine the minimum required buffer size for the successful creation of an AudioRecord instance. Using values smaller than getMinBufferSize() will result in an initialization failure.
    • getMinBufferSize
      int getMinBufferSize (
            int sampleRateInHz,
            int channelConfig,
            int audioFormat)

      Returns the minimum buffer size required for the successful creation of an AudioRecord object, in byte units.
            :
    • startRecording
      void startRecording ()

      Starts recording from the AudioRecord instance.
    • read
      int read (short[] audioData,
            int offsetInShorts,
            int sizeInShorts)

      Reads audio data from the audio hardware for recording into a short array. The format specified in the AudioRecord constructor should be ENCODING_PCM_16BIT to correspond to the data in the array.
            :
    • getRecordingState
      int getRecordingState ()

      Returns the recording state of the AudioRecord instance.

      See also:
            RECORDSTATE_STOPPED
            RECORDSTATE_RECORDING
    • stop
      void stop ()

      Stops recording.

出力:AudioTrack クラス

  • AudioTrack
    The AudioTrack class manages and plays a single audio resource for Java applications. It allows streaming of PCM audio buffers to the audio sink for playback.
          :
    An AudioTrack instance can operate under two modes: static or streaming. In Streaming mode, the application writes a continuous stream of data to the AudioTrack, using one of the write() methods.
          :
    The static mode should be chosen when dealing with short sounds that fit in memory and that need to be played with the smallest latency possible.
          :
    • AudioTrack (constructor)
      AudioTrack (
            int streamType,
            int sampleRateInHz,
            int channelConfig,
            int audioFormat,
            int bufferSizeInBytes,
            int mode)
            :
      Parameters
            :
      int: the total size (in bytes) of the internal buffer where audio data is read from for playback. This should be a nonzero multiple of the frame size in bytes.
            :
      If the track's creation mode is MODE_STREAM, this should be the desired buffer size for the AudioTrack to satisfy the application's latency requirements. If bufferSizeInBytes is less than the minimum buffer size for the output sink, it is increased to the minimum buffer size.
            :
      See getMinBufferSize(int, int, int) to determine the estimated minimum buffer size for an AudioTrack instance in streaming mode.
    • play
      void play ()

      Starts playing an AudioTrack.

      If track's creation mode is MODE_STATIC, you must have called one of the write methods(中略)prior to play().

      If the mode is MODE_STREAM, you can optionally prime the data path prior to calling play(), by writing up to bufferSizeInBytes (from constructor). If you don't call write() first, or if you call write() but with an insufficient amount of data, then the track will be in underrun state at play(). In this case, playback will not actually start playing until the data path is filled to a device-specific minimum level.
            :
    • write
      int write (
            short[] audioData,
            int offsetInShorts,
            int sizeInShorts)

      Writes the audio data to the audio sink for playback (streaming mode), or copies audio data for later playback (static buffer mode). The format specified in the AudioTrack constructor should be ENCODING_PCM_16BIT to correspond to the data in the array.
            :
    • getPlayState
      int getPlayState ()

      Returns the playback state of the AudioTrack instance.

      See also:
            PLAYSTATE_STOPPED
            PLAYSTATE_PAUSED
            PLAYSTATE_PLAYING
    • pause
      void pause ()

      Pauses the playback of the audio data. Data that has not been played back will not be discarded. Subsequent calls to play() will play this data back. See flush() to discard this data.
            :
    • stop
      void stop ()

      Stops playing the audio data.
            :
    • flush
      void flush ()

      Flushes the audio data currently queued for playback. Any data that has been written but not yet presented will be discarded. No-op if not stopped or paused, or if the track's creation mode is not MODE_STREAM.
            :

試作

Android 環境での音声入出力の基本的な作法を覚えるために最初にごくシンプルなアプリを書いてみました。 入力側と出力側は多くの場合機能的に分けられますが、ここではその両方の要素を次の内容で単一のプログラムに組み入れています。

  • UI 上のボタンを押下するとマイクロフォンからの録音を開始する
  • 再度ボタンを押下すると録音を終了しその内容を再生する
  • 音声データはメモリ上で扱いファイルは使用しない

処理のイメージ

   

動作の様子

動画:33秒  ※音量注意!

ソースコード

sonic01     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic01
     *
     * 端末のマイクから音声を録音しスピーカーで再生する
     * Android 標準の AudioRecord, AudioTrack を使用
     *
     * サンプリング周波数 44.1kHz
     * 量子化ビット数 16
     * モノラル
     *
     */
    
    package jp.klab.sonic01;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.ProgressBar;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final int BLOCK_NUMBER = 300;
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_PLAY_END   = 130;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
      private AudioTrack mAudioTrack = null;
    
      private Button mButton01;
      private ProgressBar mProgressBar;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mPlayBuf[];
      private short mRecordBuf[];
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mProgressBar = (ProgressBar)findViewById(R.id.progressBar);
        mProgressBar.setMax(BLOCK_NUMBER);
        mProgressBar.setProgress(0);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 録音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
        // 再生用バッファ
        mPlayBuf = new short[mBufferSizeInShort * BLOCK_NUMBER];
    
        // 録音用
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        }
        return;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setEnabled(false);
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButton01.setEnabled(true);
            mButton01.setText("START");
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        int count = 0;
        while (mInRecording && !mStop) {
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          // 再生用バッファはリングバッファとして扱う
          if (count * mBufferSizeInShort >= mPlayBuf.length) {
            count = 0;
            mProgressBar.setProgress(0);
          }
          // 再生用バッファへ集音したデータをアペンド
          System.arraycopy(mRecordBuf, 0, mPlayBuf, count * mBufferSizeInShort, mBufferSizeInShort);
          mProgressBar.setProgress(++count);
        }
        // 集音終了
        mAudioRecord.stop();
        mProgressBar.setProgress(0);
        mHandler.sendEmptyMessage(MSG_RECORD_END);
        if (mStop) {
          return;
        }
        // 再生
        mAudioTrack.setPlaybackRate(SAMPLE_RATE);
        mAudioTrack.play();
        mAudioTrack.write(mPlayBuf, 0, count * mBufferSizeInShort);
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    }
    
    
  • AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.klab.sonic01">
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
          android:name=".MainActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|screenSize"
          android:screenOrientation="unspecified"
          android:theme="@style/AppTheme.NoActionBar">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
           App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
        <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      </application>
    </manifest>
    
    

準備 2. 任意の周波数の音波を生成・出力する

音による情報伝達(言語音を除く)においては、伝えたい内容の意味を構成する要素として「音の高さ(周波数)」と「音の大きさ(振幅)」が挙げられます。これらを所定の規則に基づいて組み合わせることで送り手と受け手の間でのやりとりが可能となるでしょう。送り手側にはそこで任意の周波数・振幅の音を作る処理が必要となります。これがふたつめの課題です。

サイン波について

音の波形には種類がありますが、ここでは基本波形のひとつであるサイン波を使うことにしました。サイン波をプログラムで生成するためにその性質・特徴と関連する情報を整理してみます。

  • 正弦波 - wikipedia より
    正弦波(せいげんは、sine wave、sinusoidal wave)は、正弦関数として観測可能な周期的変化を示す波動のことである。その波形は正弦曲線(せいげんきょくせん、sine curve)もしくはシヌソイド (Sinusoid) と呼ばれ、数学、信号処理、電気工学およびその他の分野において重要な働きをする。       :
  • 正弦波交流って、実は円運動なんです - manapo.com 様のサイトより
    ある点を中心に回転する運動は「円運動」と言われ、 物理の世界ではよく出てくる言葉でもある。この円運動の軌跡を回転角度を横軸にしてグラフにすると、このように正弦波の形になる。
    角度が90°のところで山になり、180°のところで出発点と同じになり、270°のところで谷になり、360°のところで元に戻る。

    図では回転角度ではなく、少しでも理解しやすいように時間としてある。言い換えると、円運動で1回転するのに必要な時間を周期と呼ぶのである。そして、1秒間の回転数が即ち周波数である。
  •  https://www.youtube.com/embed/ij0K2tUVEGs - surikenclub 様による Youtube 動画
  • 第5章 フーリエ級数 - Masasi.Sanae 様のサイトより
    ・1秒間に描かれる波の回数を「周波数」といい,単位はヘルツ(Hz)を用います。
    ・1秒間に進む角度のことを「角速度」といいω(オメガ)で表します。
    ・横軸からの波の振れ具合を「振幅」といいます。

    sin波,cos波の1つの波が現れる角度(周期といいます)は360°ですから

        角速度=周波数×360°

    が成り立ちます。次の例では周波数が3,角速度1080°,振幅1のsin波を表しています。
    周波数k,振幅aのsin波はy=a sin kωtで表されます。(ω=360°)

    y=sin 1080°t=sin 3ωt

  • ラジアン(弧度法) - wikipedia より
    ラジアン(radian, 単位記号: [rad])は、国際単位系 (SI) における角度(平面角)の単位である。円周上でその円の半径と同じ長さのを切り取る2本の半径が成す角の値と定義される。

    1ラジアンは度数法では (180/π)° で、およそ 57.29578° に相当する。180°は弧度法においては π rad であり、360° は 2π rad である。
          :
    数学で三角関数を扱う時は、角度にラジアンを用いるのが普通である。その理由は、それが「自然」だからであり、多くの公式が簡潔に書けるようになる。

    (※半径 r の円の円周は 2 π r であるため 円(=360°)を弧度法で表すと「2 π r ÷ r」。よって 360° = 2π ラジアン)

以上のことから、振幅が A、角速度 ω のサイン波信号のある時点 t 秒における値 y は次の式によって得られます。

    y = A * sin ( ω * t )

ここで、角速度 = 周波数( f Hz )× 360° であり 360° = 2π ラジアン であるため角速度は「周波数( f Hz ) × 2π」と表せるためこの式は次のようにも書けます。

    y = A * sin ( 2π * f * t )

サンプリングレートについて

所定の周波数の信号を出力するためには相応のサンプリング(標本化)レートが必要となります。

オーディオ CD の規格で採用されているサンプリングレート 44,100Hz であれば理論上 22,000Hz 付近までの再現が可能であるため実用上の不足はないでしょう。また、十分なダイナミックレンジを確保するために振幅情報の量子化にはこれもオーディオ CD と同様に 16ビットを用いることとします。(ちなみに DVD は 24ビットだそうです)

計算してみると、「サンプリングレート 44,100Hz・量子化ビット数 16・モノラル」で 1秒間の信号を構成するには、44,100点 * 2バイト * 1チャネル = 88,200バイトのメモリ空間が必要ということになります。

関連情報

試作

以上の情報をもとにプログラムを試作しました。200Hz 〜 14,000Hz の範囲の 10種類の周波数のサイン波信号を生成・出力するものです。ここでは要素数 44,100 の short 型配列に 1秒分のサイン波データを格納しループ再生を行っています。

動作の様子

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


分析に使用しているアプリ: AudioUtil Spectrum Analyzer - play.google.com

ソースコード

sonic03     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic03
     *
     * サイン波信号を生成して鳴らす
     *
     * サンプリング周波数 44.1kHz
     * 量子化ビット数 16
     * モノラル
     *
     */
    
    package jp.klab.sonic03;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.ToggleButton;
    
    import java.util.Arrays;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final float SEC_PER_SAMPLEPOINT = 1.0f / SAMPLE_RATE;
      private static final int AMP = 4000;
    
      private static final int [] FREQS =
          new int[] {200, 400, 800, 1600, 2000, 4000, 8000, 10000, 12000, 14000};
      private static final int DO = 262 * 2;
      private static final int RE = 294 * 2;
      private static final int MI = 330 * 2;
      private static final int FA = 349 * 2;
      private static final int SO = 392 * 2;
      private static final int RA = 440 * 2;
      private static final int SI = 494 * 2;
    
      private static final int MSG_PLAY_START = 120;
      private static final int MSG_PLAY_END   = 130;
    
      private Handler mHandler;
      private AudioTrack mAudioTrack = null;
      private ToggleButton mButtons[];
      private int mIdxButtonPushed = -1;
      private int mBufferSizeInShort;
      private short mPlayBuf[];
    
      // 1秒分のサイン波データを生成
      private void createSineWave(int freq, int amplitude, boolean doClear) {
        if (doClear) {
          Arrays.fill(mPlayBuf, (short) 0);
        }
        for (int i = 0; i < SAMPLE_RATE; i++) {
          float currentSec = i * SEC_PER_SAMPLEPOINT; // 現在位置の経過秒数
          // y(t) = A * sin(2π * f * t)
          double val = amplitude * Math.sin(2.0 * Math.PI * freq * currentSec);
          mPlayBuf[i] += (short)val;
        }
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButtons = new ToggleButton[11];
        (mButtons[0] = (ToggleButton)findViewById(R.id.button01)).setOnClickListener(this);
        (mButtons[1] = (ToggleButton)findViewById(R.id.button02)).setOnClickListener(this);
        (mButtons[2] = (ToggleButton)findViewById(R.id.button03)).setOnClickListener(this);
        (mButtons[3] = (ToggleButton)findViewById(R.id.button04)).setOnClickListener(this);
        (mButtons[4] = (ToggleButton)findViewById(R.id.button05)).setOnClickListener(this);
        (mButtons[5] = (ToggleButton)findViewById(R.id.button06)).setOnClickListener(this);
        (mButtons[6] = (ToggleButton)findViewById(R.id.button07)).setOnClickListener(this);
        (mButtons[7] = (ToggleButton)findViewById(R.id.button08)).setOnClickListener(this);
        (mButtons[8] = (ToggleButton)findViewById(R.id.button09)).setOnClickListener(this);
        (mButtons[9] = (ToggleButton)findViewById(R.id.button10)).setOnClickListener(this);
        (mButtons[10] = (ToggleButton)findViewById(R.id.button11)).setOnClickListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
        // 再生用バッファ
        mPlayBuf = new short[SAMPLE_RATE]; // 1秒分のバッファを確保
    
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        for (int i = 0; i < mButtons.length; i++) {
          if (mButtons[i] == (ToggleButton)v) {
            mIdxButtonPushed = i;
            break;
          }
        }
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        if (mButtons[mIdxButtonPushed].isChecked()) {
          new Thread(this).start();
        }
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_PLAY_START:
            Log.d(TAG, "MSG_PLAY_START");
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButtons[mIdxButtonPushed].setChecked(false);
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_PLAY_START);
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        mAudioTrack.play();
        if (mIdxButtonPushed != 10) {
          createSineWave(FREQS[mIdxButtonPushed], AMP, true);
          for (int i = 0; i < 5; i++) { // 5秒程度鳴らす
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE);
          }
        } else { // SONG
          for (int i = 0; i < 2; i++) {
            mAudioTrack.play();
            createSineWave(DO, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(FA, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(DO, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE);
    
            // 和音
            createSineWave(DO, AMP, true);
            createSineWave(MI, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            createSineWave(FA, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            createSineWave(SO, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(FA, AMP, true);
            createSineWave(RA, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            createSineWave(SO, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            createSineWave(FA, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(DO, AMP, true);
            createSineWave(MI, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE);
          }
        }
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    }
    
    
  • AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.klab.sonic03">
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
          android:name=".MainActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|screenSize"
          android:screenOrientation="unspecified"
          android:theme="@style/AppTheme.NoActionBar">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
           App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
        <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      </application>
    </manifest>
    
    

スプレッドシートを使っての検証

前述のように上のコードの createSineWave 関数は要素数 44,100 の short 型配列に 44,100Hz・16ビット・モノラル 1秒分のサイン波データを生成しています。

この配列の先頭 441 要素分、つまり 1/100 秒分のデータとそれをグラフ化したものを以下に示します。それぞれ期待通りの波形が生成されていることが見てとれます。

  • 周波数 400Hz を指定した場合:400Hz/100 につき 4 周期分の出力
    element,value
    0,0
    1,227
    2,454
    3,680
    4,903
    5,1124
    6,1341
    7,1553
    8,1761
    9,1962
    10,2158
    11,2346
    12,2527
    13,2699
    14,2863
    15,3017
    16,3162
    17,3296
    18,3420
    19,3533
    20,3634
    21,3723
    22,3800
    23,3865
    24,3917
    25,3957
    26,3984
    27,3997
    28,3998
    29,3986
    30,3961
    31,3923
    32,3872
    33,3809
    34,3733
    35,3646
    36,3546
    37,3435
    38,3312
    39,3179
    40,3036
    41,2883
    42,2720
    43,2549
    44,2369
    45,2182
    46,1987
    47,1786
    48,1579
    49,1368
    50,1151
    51,931
    52,708
    53,483
    54,256
    55,28
    56,-199
    57,-426
    58,-652
    59,-876
    60,-1097
    61,-1314
    62,-1527
    63,-1735
    64,-1937
    65,-2134
    66,-2323
    67,-2505
    68,-2678
    69,-2843
    70,-2999
    71,-3145
    72,-3280
    73,-3405
    74,-3519
    75,-3622
    76,-3712
    77,-3791
    78,-3858
    79,-3911
    80,-3953
    81,-3981
    82,-3996
    83,-3999
    84,-3988
    85,-3965
    86,-3928
    87,-3879
    88,-3818
    89,-3743
    90,-3657
    91,-3559
    92,-3449
    93,-3328
    94,-3197
    95,-3055
    96,-2902
    97,-2741
    98,-2571
    99,-2392
    100,-2205
    101,-2012
    102,-1812
    103,-1606
    104,-1394
    105,-1179
    106,-959
    107,-736
    108,-511
    109,-284
    110,-56
    111,170
    112,398
    113,624
    114,848
    115,1069
    116,1287
    117,1501
    118,1709
    119,1913
    120,2110
    121,2300
    122,2482
    123,2657
    124,2823
    125,2980
    126,3127
    127,3264
    128,3390
    129,3506
    130,3610
    131,3702
    132,3782
    133,3850
    134,3905
    135,3948
    136,3978
    137,3995
    138,3999
    139,3990
    140,3968
    141,3934
    142,3886
    143,3826
    144,3753
    145,3669
    146,3572
    147,3464
    148,3344
    149,3214
    150,3073
    151,2922
    152,2762
    153,2592
    154,2415
    155,2229
    156,2036
    157,1837
    158,1632
    159,1421
    160,1206
    161,987
    162,764
    163,539
    164,313
    165,85
    166,-142
    167,-369
    168,-596
    169,-820
    170,-1042
    171,-1260
    172,-1474
    173,-1684
    174,-1887
    175,-2085
    176,-2276
    177,-2460
    178,-2636
    179,-2803
    180,-2961
    181,-3109
    182,-3247
    183,-3375
    184,-3492
    185,-3597
    186,-3691
    187,-3773
    188,-3842
    189,-3899
    190,-3944
    191,-3975
    192,-3994
    193,-3999
    194,-3992
    195,-3972
    196,-3939
    197,-3893
    198,-3834
    199,-3763
    200,-3680
    201,-3585
    202,-3478
    203,-3360
    204,-3231
    205,-3091
    206,-2941
    207,-2782
    208,-2614
    209,-2437
    210,-2253
    211,-2061
    212,-1862
    213,-1658
    214,-1448
    215,-1233
    216,-1014
    217,-792
    218,-567
    219,-341
    220,-113
    221,113
    222,341
    223,567
    224,792
    225,1014
    226,1233
    227,1448
    228,1658
    229,1862
    230,2061
    231,2253
    232,2437
    233,2614
    234,2782
    235,2941
    236,3091
    237,3231
    238,3360
    239,3478
    240,3585
    241,3680
    242,3763
    243,3834
    244,3893
    245,3939
    246,3972
    247,3992
    248,3999
    249,3994
    250,3975
    251,3944
    252,3899
    253,3842
    254,3773
    255,3691
    256,3597
    257,3492
    258,3375
    259,3247
    260,3109
    261,2961
    262,2803
    263,2636
    264,2460
    265,2276
    266,2085
    267,1887
    268,1684
    269,1474
    270,1260
    271,1042
    272,820
    273,596
    274,369
    275,142
    276,-85
    277,-313
    278,-539
    279,-764
    280,-987
    281,-1206
    282,-1421
    283,-1632
    284,-1837
    285,-2036
    286,-2229
    287,-2415
    288,-2592
    289,-2762
    290,-2922
    291,-3073
    292,-3214
    293,-3344
    294,-3464
    295,-3572
    296,-3669
    297,-3753
    298,-3826
    299,-3886
    300,-3934
    301,-3968
    302,-3990
    303,-3999
    304,-3995
    305,-3978
    306,-3948
    307,-3905
    308,-3850
    309,-3782
    310,-3702
    311,-3610
    312,-3506
    313,-3390
    314,-3264
    315,-3127
    316,-2980
    317,-2823
    318,-2657
    319,-2482
    320,-2300
    321,-2110
    322,-1913
    323,-1709
    324,-1501
    325,-1287
    326,-1069
    327,-848
    328,-624
    329,-398
    330,-170
    331,56
    332,284
    333,511
    334,736
    335,959
    336,1179
    337,1394
    338,1606
    339,1812
    340,2012
    341,2205
    342,2392
    343,2571
    344,2741
    345,2902
    346,3055
    347,3197
    348,3328
    349,3449
    350,3559
    351,3657
    352,3743
    353,3818
    354,3879
    355,3928
    356,3965
    357,3988
    358,3999
    359,3996
    360,3981
    361,3953
    362,3911
    363,3858
    364,3791
    365,3712
    366,3622
    367,3519
    368,3405
    369,3280
    370,3145
    371,2999
    372,2843
    373,2678
    374,2505
    375,2323
    376,2134
    377,1937
    378,1735
    379,1527
    380,1314
    381,1097
    382,876
    383,652
    384,426
    385,199
    386,-28
    387,-256
    388,-483
    389,-708
    390,-931
    391,-1151
    392,-1368
    393,-1579
    394,-1786
    395,-1987
    396,-2182
    397,-2369
    398,-2549
    399,-2720
    400,-2883
    401,-3036
    402,-3179
    403,-3312
    404,-3435
    405,-3546
    406,-3646
    407,-3733
    408,-3809
    409,-3872
    410,-3923
    411,-3961
    412,-3986
    413,-3998
    414,-3997
    415,-3984
    416,-3957
    417,-3917
    418,-3865
    419,-3800
    420,-3723
    421,-3634
    422,-3533
    423,-3420
    424,-3296
    425,-3162
    426,-3017
    427,-2863
    428,-2699
    429,-2527
    430,-2346
    431,-2158
    432,-1962
    433,-1761
    434,-1553
    435,-1341
    436,-1124
    437,-903
    438,-680
    439,-454
    440,-227
    
  • 周波数 1,600Hz を指定した場合:1,600Hz/100 につき 16 周期分の出力
    element,value
    0,0
    1,903
    2,1761
    3,2527
    4,3162
    5,3634
    6,3917
    7,3998
    8,3872
    9,3546
    10,3036
    11,2369
    12,1579
    13,708
    14,-199
    15,-1097
    16,-1937
    17,-2678
    18,-3280
    19,-3712
    20,-3953
    21,-3988
    22,-3818
    23,-3449
    24,-2902
    25,-2205
    26,-1394
    27,-511
    28,398
    29,1287
    30,2110
    31,2823
    32,3390
    33,3782
    34,3978
    35,3968
    36,3753
    37,3344
    38,2762
    39,2036
    40,1206
    41,313
    42,-596
    43,-1474
    44,-2276
    45,-2961
    46,-3492
    47,-3842
    48,-3994
    49,-3939
    50,-3680
    51,-3231
    52,-2614
    53,-1862
    54,-1014
    55,-113
    56,792
    57,1658
    58,2437
    59,3091
    60,3585
    61,3893
    62,3999
    63,3899
    64,3597
    65,3109
    66,2460
    67,1684
    68,820
    69,-85
    70,-987
    71,-1837
    72,-2592
    73,-3214
    74,-3669
    75,-3934
    76,-3995
    77,-3850
    78,-3506
    79,-2980
    80,-2300
    81,-1501
    82,-624
    83,284
    84,1179
    85,2012
    86,2741
    87,3328
    88,3743
    89,3965
    90,3981
    91,3791
    92,3405
    93,2843
    94,2134
    95,1314
    96,426
    97,-483
    98,-1368
    99,-2182
    100,-2883
    101,-3435
    102,-3809
    103,-3986
    104,-3957
    105,-3723
    106,-3296
    107,-2699
    108,-1962
    109,-1124
    110,-227
    111,680
    112,1553
    113,2346
    114,3017
    115,3533
    116,3865
    117,3997
    118,3923
    119,3646
    120,3179
    121,2549
    122,1786
    123,931
    124,28
    125,-876
    126,-1735
    127,-2505
    128,-3145
    129,-3622
    130,-3911
    131,-3999
    132,-3879
    133,-3559
    134,-3055
    135,-2392
    136,-1606
    137,-736
    138,170
    139,1069
    140,1913
    141,2657
    142,3264
    143,3702
    144,3948
    145,3990
    146,3826
    147,3464
    148,2922
    149,2229
    150,1421
    151,539
    152,-369
    153,-1260
    154,-2085
    155,-2803
    156,-3375
    157,-3773
    158,-3975
    159,-3972
    160,-3763
    161,-3360
    162,-2782
    163,-2061
    164,-1233
    165,-341
    166,567
    167,1448
    168,2253
    169,2941
    170,3478
    171,3834
    172,3992
    173,3944
    174,3691
    175,3247
    176,2636
    177,1887
    178,1042
    179,142
    180,-764
    181,-1632
    182,-2415
    183,-3073
    184,-3572
    185,-3886
    186,-3999
    187,-3905
    188,-3610
    189,-3127
    190,-2482
    191,-1709
    192,-848
    193,56
    194,959
    195,1812
    196,2571
    197,3197
    198,3657
    199,3928
    200,3996
    201,3858
    202,3519
    203,2999
    204,2323
    205,1527
    206,652
    207,-256
    208,-1151
    209,-1987
    210,-2720
    211,-3312
    212,-3733
    213,-3961
    214,-3984
    215,-3800
    216,-3420
    217,-2863
    218,-2158
    219,-1341
    220,-454
    221,454
    222,1341
    223,2158
    224,2863
    225,3420
    226,3800
    227,3984
    228,3961
    229,3733
    230,3312
    231,2720
    232,1987
    234,256
    235,-652
    236,-1527
    237,-2323
    238,-2999
    239,-3519
    240,-3858
    241,-3996
    242,-3928
    243,-3657
    244,-3197
    245,-2571
    246,-1812
    247,-959
    248,-56
    249,848
    250,1709
    251,2482
    252,3127
    253,3610
    254,3905
    255,3999
    256,3886
    257,3572
    258,3073
    259,2415
    260,1632
    261,764
    262,-142
    263,-1042
    264,-1887
    265,-2636
    266,-3247
    267,-3691
    268,-3944
    269,-3992
    270,-3834
    271,-3478
    272,-2941
    273,-2253
    274,-1448
    275,-567
    276,341
    277,1233
    278,2061
    279,2782
    280,3360
    281,3763
    282,3972
    283,3975
    284,3773
    285,3375
    286,2803
    287,2085
    288,1260
    289,369
    290,-539
    291,-1421
    292,-2229
    293,-2922
    294,-3464
    295,-3826
    296,-3990
    297,-3948
    298,-3702
    299,-3264
    300,-2657
    302,-1069
    303,-170
    304,736
    305,1606
    306,2392
    307,3055
    308,3559
    309,3879
    310,3999
    311,3911
    312,3622
    313,3145
    314,2505
    315,1735
    316,876
    317,-28
    318,-931
    319,-1786
    320,-2549
    321,-3179
    322,-3646
    323,-3923
    324,-3997
    325,-3865
    326,-3533
    327,-3017
    328,-2346
    329,-1553
    330,-680
    331,227
    332,1124
    333,1962
    334,2699
    335,3296
    336,3723
    337,3957
    338,3986
    339,3809
    340,3435
    341,2883
    342,2182
    343,1368
    344,483
    345,-426
    346,-1314
    347,-2134
    348,-2843
    349,-3405
    350,-3791
    351,-3981
    352,-3965
    353,-3743
    354,-3328
    355,-2741
    356,-2012
    357,-1179
    358,-284
    359,624
    360,1501
    361,2300
    362,2980
    363,3506
    364,3850
    365,3995
    366,3934
    367,3669
    368,3214
    369,2592
    370,1837
    371,987
    372,85
    373,-820
    374,-1684
    375,-2460
    376,-3109
    377,-3597
    378,-3899
    379,-3999
    380,-3893
    381,-3585
    382,-3091
    383,-2437
    384,-1658
    385,-792
    386,113
    387,1014
    388,1862
    389,2614
    390,3231
    391,3680
    392,3939
    393,3994
    394,3842
    395,3492
    396,2961
    397,2276
    398,1474
    399,596
    400,-313
    401,-1206
    402,-2036
    403,-2762
    404,-3344
    405,-3753
    406,-3968
    407,-3978
    408,-3782
    409,-3390
    410,-2823
    411,-2110
    412,-1287
    413,-398
    414,511
    415,1394
    416,2205
    417,2902
    418,3449
    419,3818
    420,3988
    421,3953
    422,3712
    423,3280
    424,2678
    425,1937
    426,1097
    427,199
    428,-708
    429,-1579
    430,-2369
    431,-3036
    432,-3546
    433,-3872
    434,-3998
    435,-3917
    436,-3634
    437,-3162
    438,-2527
    439,-1761
    440,-903
    

準備 3. 集音したデータの周波数分析を行う

FFT(Fast Fourier Transform 高速フーリエ変換)について

無響室のように人為的な特殊環境を除き世界は雑多な音で満ちています。人間の脳は自身が無意識のうちに複数の音の中から必要な音を選り分けており(カクテルパーティー効果)、静かに感じられる室内であってもレコーダーでの録音内容を再生すると意外なノイズに気づくといった経験は一般的なものですね。そのため、受け手側の装置が特定の音を検知するためには必要なものだけを明示的にピックアップする必要があります。これがみっつめの課題です。広く知られるフーリエ変換を音声信号に適用すれば音の塊を周波数成分ごとに分析できます。

フーリエ変換とは

  • フーリエ変換の本質 - apphokuson 様のサイトより

    全ての信号は、上図のように波の足しあわせで表現することが出来ます。

    具体的には、周波数が1の波1と周波数が2の波2と周波数が3の波3と・・・周波数が nの波nを足し合わせることで、あらゆる信号を表現することが出来るのです。
          :
    周波数を横軸に、振幅の大きさ(パワー値)を縦軸にとってグラフを書きなおしてみます。
          :
    この結果は、左図の信号に周波数1の波が大きさ4で含まれていること、周波数2の 波が大きさ0.5で含まれていること、周波数3の波が・・・ということを意味しています。
          :
    フーリエ変換を行えば、「信号に含まれる周波数の成分比」を得ることが出来ます。
          :

    (※引用に際し表記に一部手を加えさせて頂きました tanabe)

    • 統計的音声認識の基本原理 - 篠崎隆宏様のサイトより
      音声信号は,一次元の時系列データです.横軸を時間,縦軸を音の大きさとしてグラフを作成すると,以下のような図が得られます.

高速フーリエ変換とは

  • 高速フーリエ変換 - wikipedia より
    高速フーリエ変換(こうそくフーリエへんかん、英: Fast Fourier Transform、FFT) とは、離散フーリエ変換 (Discrete Fourier Transform、DFT) を計算機上で高速に 計算するアルゴリズム。FFTの逆変換をIFFT (Inverse FFT) と呼ぶ。
    • 離散フーリエ変換入門 - EnergyChord 様のサイトより
      コンピュータで行われるフーリエ変換は基本的に離散フーリエ変換である.なぜなら,電子計算機は連続量をそのまま扱うことができないため,コンピュータが処理するデータは必ず離散化される必要があるからである.つまり,コンピュータによるデータ処理が当たり前となった現代技術においては,フーリエ変換と言えば離散フーリエ変換を指すと言っても過言ではない.
      • 離散化とは - IT用語辞典バイナリより
        離散化とは、ある連続した情報を、非連続の値に分割することである。
        連続した値を持った情報を解析することは非常に困難であるが、離散化を行い非連続な数値に置き換えることで、近似的な計算結果を比較的容易に算出することが可能となる。
    • フーリエ変換と離散フーリエ変換 - 岩本真裕子様のサイトより
            :
      フーリエ変換では、波y(t)を構成する三角関数とその周波数・振幅・初期位相などを知ることができるため、波の特徴を明確に捉えることができる。

      離散フーリエ変換

      これまでは、連続関数に対するフーリエ変換およびフーリエ積分(逆フーリエ変換)について考えてきた。実際、音声や音楽などは、連続的な波として出力される。しかし、コンピュータは離散値でしか扱えないため、Analog-Didital (AD) 変換器を使って、一定間隔でサンプリングし、コンピュータが直接扱える離散的な波に変換される。
            :


      連続的な波と離散的な波の関係

      フーリエ変換は、連続的な波を三角関数を重ね合わせることで表そうとするものであった。例えば、下図のように、8個の周期関数を重ね合わせたものが実際の音であったとする。


      次に、AD変換によって、1秒間に4個のサンプリング数(4等分)で連続的な波が離散化されたとする(黒丸)。よく見てみると、cos 3t の離散の4点は cos t の離散の4点と全く同じである。実は sin 3t と sin t についても同様である。
      つまり、離散化された波においては、高周波数の波は低周波数の波に吸収されてしまう。今の場合は、重ね合わせられた複雑な波が、k=0,1,2 の三角関数のみで表せるということになってしまう。
      しかし、容易に予想できるように、サンプリング数を 増やすと、この問題は解消する。
            :
      離散フーリエ変換を用いた音などの解析や加工には、サンプリングは非常に重要な要素であり、扱う波についてある程度特徴や性質を知っている必要がある。例えば、人間が聞くことができる音の周波数は、20 Hzから20 kHzと言われている(一般的には15 kHzくらいまで)ので、人間が聞くことができるすべての周波数をカバーしようとすると、サンプリング数は1秒間あたり、20000(Hz)×2=40000 回必要となる。
            :

FFT ライブラリ「JTransforms」

Android には標準の Visualizer クラスに FFT 用の API getFft() が用意されています。ただし、あくまでも「The Visualizer class enables application to retrieve part of the currently playing audio for visualization purpose.」であり、「Frequency data: 8-bit magnitude FFT by using the getFft(byte[]) method」と機能的にも控えめな内容となっています。 Android で使える FFT ライブラリには複数の選択肢がありますが、Piotr Wendykier 氏による JTransforms が高性能かつメジャーであることを知りこれを利用することにしました。

  • JTransforms - Piotr Wendykier - sites.google.com
    JTransforms is the first, open source, multithreaded FFT library written in pure Java. Currently, four types of transforms are available: Discrete Fourier Transform (DFT), Discrete Cosine Transform (DCT), Discrete Sine Transform (DST) and Discrete Hartley Transform (DHT). The code is derived from General Purpose FFT Package written by Takuya Ooura and from Java FFTPack written by Baoshe Zhang.

    (Google 訳)
    JTransformsは、純粋なJavaで書かれた最初のオープンソースのマルチスレッド FFTライブラリです。現在、離散フーリエ変換(DFT)、離散コサイン変換(DCT)、 離散サイン変換(DST)および離散ハートレー変換(DHT)の4種類の変換が利用可能 である。このコードは、Takuya Ooura が書いた汎用FFTパッケージと、Baoshe Zhangが 書いたJava FFTPackから得られたものです。
          :
    License
    JTransforms is distributed under the terms of the BSD-2-Clause license.
  • wendykierp/JTransforms - GitHub
    • Documentation | Overview (JTransforms 3.1 API) - wendykierp.github.io
      JTransforms 3.1 API
      Packages
      Package                Description
      org.jtransforms.dct    Discrete Cosine Transforms.
      org.jtransforms.dht    Discrete Hartley Transforms.
      org.jtransforms.dst    Discrete Sine Transforms.
      org.jtransforms.fft    Discrete Fourier Transforms.
      org.jtransforms.utils  Utility classes.
      • Class DoubleFFT_1D
        org.jtransforms.fft
        Class DoubleFFT_1D

        Computes 1D Discrete Fourier Transform (DFT) of complex and real,double precision data. The size of the data can be an arbitrary number.

        (Google 訳)
        複素数と実数の倍精度データの一次元離散フーリエ変換(DFT)を計算します。データのサイズは任意の数にすることができます。
前掲のように音声信号は一次元データなので使うのはこの DoubleFFT_1D クラスでよさそうですが Javadoc では利用方法がさっぱりわからず使用例をネットで探しました。
  • JTransforms FFT in Android from PCM data - stackoverflow.com
      System.arraycopy(applyWindow(sampleData), 0, a, 0, sampleData.length);
      fft.realForward(a);

      /* find the peak magnitude and it's index */
      double maxMag = Double.NEGATIVE_INFINITY;
      int maxInd = -1;

      for(int i = 0; i < a.length / 2; ++i) {
        double re  = a[2*i];
        double im  = a[2*i+1];
        double mag = Math.sqrt(re * re + im * im);

        if(mag > maxMag) {
          maxMag = mag;
          maxInd = i;
        }
      }
  • How to get frequency from fft result? - stackoverflow.com
    The complex data is interleaved, with real components at even indices and imaginary components at odd indices, i.e. the real components are at index 2*i, the imaginary components are at index 2*i+1.

    To get the magnitude of the spectrum at index i, you want:

    re = fft[2*i];
    im = fft[2*i+1];
    magnitude[i] = sqrt(re*re+im*im);

    Then you can plot magnitude[i] for i = 0 to N / 2 to get the power spectrum. Depending on the nature of your audio input you should see one or more peaks in the spectrum.

    To get the approximate frequency of any given peak you can convert the index of the peak as follows:

    freq = i * Fs / N;

    where:

    freq = frequency in Hz
    i = index of peak
    Fs = sample rate (e.g. 44100 Hz or whatever you are using)
    N = size of FFT (e.g. 1024 in your case)

なるほど、使い方は難しくないようです。整理してみます。

  • 音声情報(=実数のみで構成される)を FFT にかける場合は信号データの配列を引数として realForward メソッドを呼び出す
  • 実行結果は配列に複素数で上書きされ偶数インデックスには実部、ひとつ後の奇数インデックスには虚部、というペアが続く
  • 各周波数成分の振幅値は当該複素数の絶対値re2+im2 で得られる
    • 複素平面 - KIT数学ナビゲーション様のサイトより
      xy 平面において, x 軸に実数, y 軸に虚数を対応させて,複素数を表したものを複素平面という.または,複素数平面,ガウス平面ともいう.

      複素数 z=a+ⅈb を複素平面上に表したものが,右の図である.

      複素数z の絶対値の定義:

      | z |=| a+b |= a 2 + b 2 =r

      すなわち,複素平面上の原点Oから z までの距離 r となる.
  • 周波数は「配列要素のインデックス * サンプリングレート / 信号データの要素数(=「FFT サイズ」)」の式で得られる
  • つまり、もっとも振幅の大きい周波数(ピーク周波数)を得るには最大振幅値を持つ配列要素のインデックスを上の式に突っ込めばよい

試作

上記の JTransforms ライブラリを利用して集音データ中のピーク周波数を画面に表示するアプリを作成。期待どおりの結果が得られました。

動作の様子

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

ソースコード

sonic04     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic04
     *
     * 端末のマイクから音声を受信しピーク周波数を表示する
     * FFT 処理に JTransforms ライブラリを利用
     *
     * サンプリング周波数 44.1kHz
     * 量子化ビット数 16
     * モノラル
     *
     */
    
    package jp.klab.sonic04;
    
    
    import org.jtransforms.fft.DoubleFFT_1D;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final short THRESHOLD_AMP = 0x00ff;
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_FREQ_PEAK  = 120;
      private static final int MSG_SILENCE    = 130;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
    
      private Button mButton01;
      private TextView mTextView02;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mRecordBuf[];
      private DoubleFFT_1D mFFT;
      private double mFFTBuffer[];
      private int mFFTSize;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mTextView02 = (TextView)findViewById(R.id.textView02);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 録音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
    
        // FFT 処理用
        mFFTSize = mBufferSizeInShort;
        mFFT = new DoubleFFT_1D(mFFTSize);
        mFFTBuffer = new double[mFFTSize];
    
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        }
        return;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setText("START");
            break;
          case MSG_FREQ_PEAK:
            mTextView02.setText(Integer.toString(msg.arg1) + " Hz");
            break;
          case MSG_SILENCE:
            mTextView02.setText("");
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        boolean bSilence = false;
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        while (mInRecording && !mStop) {
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          bSilence = true;
          for (int i = 0; i < mBufferSizeInShort; i++) {
            short s = mRecordBuf[i];
            if (s > THRESHOLD_AMP) {
              bSilence = false;
            }
          }
          if (bSilence) { // 静寂
            mHandler.sendEmptyMessage(MSG_SILENCE);
            continue;
          }
          int freq = doFFT(mRecordBuf);
          Message msg = new Message();
          msg.what = MSG_FREQ_PEAK;
          msg.arg1 = freq;
          mHandler.sendMessage(msg);
        }
        // 集音終了
        mAudioRecord.stop();
        mHandler.sendEmptyMessage(MSG_RECORD_END);
      }
    
      private int doFFT(short[] data) {
        for (int i = 0; i < mFFTSize; i++) {
          mFFTBuffer[i] = (double)data[i];
        }
        // FFT 実行
        mFFT.realForward(mFFTBuffer);
    
        // 処理結果の複素数配列から各周波数成分の振幅値を求めピーク分の要素番号を得る
        double maxAmp = 0;
        int index = 0;
        for (int i = 0; i < mFFTSize/2; i++) {
          double a = mFFTBuffer[i*2]; // 実部
          double b = mFFTBuffer[i*2 + 1]; // 虚部
          // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
          double r = Math.sqrt(a*a + b*b);
          if (r > maxAmp) {
            maxAmp = r;
            index = i;
          }
        }
        // 要素番号・サンプリングレート・FFT サイズからピーク周波数を求める
        return index * SAMPLE_RATE / mFFTSize;
      }
    }
    
    
  • AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.klab.sonic04">
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
          android:name=".MainActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|screenSize"
          android:screenOrientation="unspecified"
          android:theme="@style/AppTheme.NoActionBar">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
           App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
        <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      </application>
    </manifest>
    
    

次回の予定

以上の内容により Android デバイスで「任意の周波数・振幅の音を出すこと」と「拾った音に含まれる周波数を検知すること」ができるようになりました。音を使ってデバイス間で通信を行うための素材がこれで揃ったことになります。次回はここまでの到達点を応用してビットデータの送受信を試みます。


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