
第5回 超音波通信ライブラリ作成
これまでに超音波の送信、受信、解析と紹介してきました。これらの技術を応用して、超音波でデータ通信を行なうライブラリを紹介します。
データ送受信の仕組み
今回紹介するのは、「データ送信ライブラリ」と、「データ受信ライブラリ」の2つです。これら2つのライブラリをそれぞれ送信側アプリと受信側アプリに組み込むことで、超音波によるデータの送受信が可能となります。まずは実際に動作を見てみましょう。
お分かりいただけましたでしょうか?送信側アプリ(左)で入力したデータが、受信側アプリ(右)で表示されていますね。これは、送信側アプリから「0(LOW)」と「1(HIGH)」の信号で構成されたデータを超音波で送信し、受信側アプリでその音波からデータを作り出すことで実現しています。
送信側・受信側の両者に必要な情報は、「ターゲット周波数」と「基本周期」になります。「ターゲット周波数」は、通信に使う周波数のことで、「基本周期」とは、「0/1」信号1つ分の時間的な間隔を意味します。分かりやすく図で見てみましょう。
上の図のように、ターゲット周波数の音波が発生している間は、基本周期ごとに信号「1」が送信されているものとし、逆に発生していない間は、基本周期ごとに信号「0」が送信されているものとします。このルールに従って、送信側アプリはデータから音波を生成し、受信側アプリはその音波から元のデータを生成すればよいわけです。
ここからは、送信側アプリで使用している「データ送信ライブラリ」と、受信側アプリで使用している「データ受信ライブラリ」に分けて説明していきます。
データ送信ライブラリ
まずは、送信側アプリで使用する「データ送信ライブラリ」から説明していきます。データ送信ライブラリは、パラメータで設定される「ターゲット周波数」、「基本周期」、「信号パターン」から波形データを生成して発信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
package com.tongarism.signalgenerator; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.util.Log; import java.util.ArrayList; public class SignalGenerator { private static final int SAMPLE_RATE_MIN = 4000; private static final int SAMPLE_RATE_MAX = 96000; private static final String SIGNAL_ON = "1"; private static final String SIGNAL_OFF = "0"; private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC; private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO; private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_8BIT; private static final int MODE = AudioTrack.MODE_STREAM; private static final String TAG = "SignalGenerator"; private int mSampleRate = 44100; int mTargetFreq; int mBasicPeriod; private byte[] mSignalPatternHigh; private byte[] mSignalPatternLow; private String[] mPattern; private int mSignalLength; private AudioTrack mTrack; private Thread mPlayThread; /** * @param targetFreq ターゲット周波数 (Hz) * @param basicPeriod 基本周期 (ms) * @param pattern 信号パターン (0 か 1) */ public SignalGenerator(int targetFreq, int basicPeriod, String pattern) { Log.d(TAG, "constructor invoked"); mTargetFreq = targetFreq; mBasicPeriod = basicPeriod; // サンプリング周波数を設定 setSampleRate(); // 信号パターンを設定 boolean ret = setPattern(pattern); if(!ret) { Log.e(TAG, "invalid pattern data"); return; } // HIGH・LOW信号波形を生成 mSignalPatternHigh = createWaves(basicPeriod, true); mSignalPatternLow = createWaves(basicPeriod, false); mSignalLength = mSignalPatternHigh.length; setAudioTrack(); Log.d(TAG, "initialize and settings success"); } private void setAudioTrack() { Log.d(TAG, "setAudioTrack invoked"); mTrack = new AudioTrack(STREAM_TYPE, mSampleRate, CHANNEL_CONFIG, AUDIO_FORMAT, mSignalLength, MODE); mTrack.setNotificationMarkerPosition(mSignalLength * mPattern.length); mTrack.setPlaybackPositionUpdateListener( new AudioTrack.OnPlaybackPositionUpdateListener() { public void onPeriodicNotification(AudioTrack track) { } public void onMarkerReached(AudioTrack track) { if (track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { Log.e(TAG, "play() successed"); stopPlayThread(); } } }); Log.d(TAG, "set AudioTrack success"); } public void startPlayThread() { Log.d(TAG, "startPlayThread invoked"); mPlayThread = new Thread() { @Override public void run() { mTrack.play(); for(int i = 0; i < mPattern.length; i++) { if(SIGNAL_ON.equals(mPattern[i])) { mTrack.write(mSignalPatternHigh, 0, mSignalLength); } else if(SIGNAL_OFF.equals((mPattern[i]))) { mTrack.write(mSignalPatternLow, 0, mSignalLength); } } } }; mPlayThread.start(); } private void stopPlayThread() { Log.d(TAG, "stopPlayThread invoked"); mTrack.stop(); mTrack.release(); mPlayThread = null; } public void send() { Log.d(TAG, "show invoked"); startPlayThread(); } public boolean setPattern(String rawPattern) { Log.d(TAG, "setPattern invoked"); ArrayList pattern = new ArrayList(); for(int i = 0; i < rawPattern.length(); i++) { String data = String.valueOf(rawPattern.charAt(i)); if(SIGNAL_ON.equals(data)) { pattern.add(SIGNAL_ON); } else if(SIGNAL_OFF.equals(data)) { pattern.add(SIGNAL_OFF); } } mPattern = pattern.toArray(new String[pattern.size()]); if(mPattern.length == 0) { // nothing pattern data return false; } return true; } private void setSampleRate() { Log.d(TAG, "setSampleRate invoked"); int sampleRate = mTargetFreq * 4; if(sampleRate < SAMPLE_RATE_MIN) { sampleRate = SAMPLE_RATE_MIN; } else if(sampleRate > SAMPLE_RATE_MAX) { sampleRate = SAMPLE_RATE_MAX; } mSampleRate = sampleRate; } private byte[] createWaves(int time, boolean on) { Log.d(TAG, "createWaves invoked"); int dataNum = (int)((double)mSampleRate * ((double)time / 1000.0)); byte[] data = new byte[dataNum]; boolean flag = true; for(int i = 0; i < dataNum; i = i + 2) { if(on) { if (flag) { data[i] = (byte) 0xff; } else { data[i] = (byte) 0x00; } } else { data[i] = (byte) 0x80; } data[i+1] = data[i]; flag = !flag; } return data; } } |
データ受信クラス
「データ受信クラス」は基本周期ごとに音波を解析し、受信データを通知するクラスです。ただし、単純に基本周期ごとに音声データを解析してしまうと、送信側アプリの音波送信タイミングと、受信側アプリの音波受信タイミングが合わなければ、誤った解釈をしてしまうことになります。
そこで、まず基本周期よりも短い「受信周期」を決めておき、受信周期ごとに音波を解析します。そして、基本周期内で発生したターゲット周波数の音波の割合を計算し、この結果によって「0」か「1」かを判断するように実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
package com.tongarism.signalanalyzer; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; import android.os.AsyncTask; import android.util.Log; import org.jtransforms.fft.DoubleFFT_1D; public class SignalAnalyzer { public static final String TAG = "SignalAnalyzer"; private static final int HIGH_DENSITY_THRESHOLD = 60; private static final int HIGH_POWER_THRESHOLD = 20000; private static final int RECEIVE_PERIOD = 20; private static final int SAMPLING_RATE = 44100; int mBasicPeriod; int mTargetFreq; int[] mSignalBuffer; int mBufferOffset; private AnalyzerEventListener mAnalyzerEventListener; private int mRecvBufSize; private short[] mRecvBuf; private AudioRecord mAudioRecord; private class AnalyzerTask extends AsyncTask { @Override protected Object doInBackground(Object[] params) { int output = (int) params[0]; mAnalyzerEventListener.onAnalyzeCompleted(output); return null; } } interface AnalyzerEventListener { public void onSignalPower(double[] power); public void onAnalyzeCompleted(int data); } /** * @param basicPeriod 基本周期 (ms) * @param targetFreq ターゲット周波数 (Hz) */ public SignalAnalyzer(int basicPeriod, int targetFreq) { Log.d(TAG, "Create Transfer"); mBasicPeriod = basicPeriod; int bufferSize = basicPeriod/RECEIVE_PERIOD; Log.d(TAG, "bufferSize = " + bufferSize); mSignalBuffer = new int[bufferSize]; mBufferOffset = 0; mTargetFreq = targetFreq; mAudioRecord = initAudioRecord(); } public void start() { if (mAudioRecord != null) { mAudioRecord.startRecording(); } } public void stop() { if (mAudioRecord != null) { mAudioRecord.stop(); } } private AudioRecord initAudioRecord(){ // AudioRecordのバッファサイズを受信周期分に設定 mRecvBufSize =(int)((SAMPLING_RATE * RECEIVE_PERIOD) / 1000.0) * 2; mRecvBuf = new short[mRecvBufSize / 2]; AudioRecord audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, // audioSource SAMPLING_RATE, // sampleRateInHz AudioFormat.CHANNEL_IN_MONO, // channelConfig AudioFormat.ENCODING_PCM_16BIT, // audioFormat mRecvBufSize); // bufferSizeInBytes // 通知間隔を受信周期ごとに設定 audioRecord.setPositionNotificationPeriod(mRecvBufSize / 2); // 音声データ受信時の処理 audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() { @Override public void onPeriodicNotification(AudioRecord recorder) { // 音声データを読み込み int size = mAudioRecord.read(mRecvBuf, 0, (mRecvBufSize / 2)); // FFTインスタンス生成 DoubleFFT_1D fft = new DoubleFFT_1D(size); double[] fftData = new double[size]; for (int i = 0; i < size; i++) { // データ型を変換 fftData[i] = (double) mRecvBuf[i]; } // FFTの実施 fft.realForward(fftData); double[] power = new double[size / 2]; double targetPower = 0; for (int i = 0; i < size; i += 2) { // 各周波数ごとの音波の強さを計算 power[i / 2] = (int) Math.sqrt(Math.pow(fftData[i], 2) + Math.pow(fftData[i + 1], 2)); int freq = (int) (((double) SAMPLING_RATE / (double) size) * i / 2); // ターゲット周波数の音波の強さを保存 int gap = Math.abs(freq - mTargetFreq); if (gap < ((double) SAMPLING_RATE / (double) size)) { targetPower = power[i / 2]; } } // ターゲット周波数の音波の強さが閾値を超えていれば、HIGH信号と判断する boolean isDetect = false; if (targetPower > HIGH_POWER_THRESHOLD) { Log.d(TAG, "detect"); isDetect = true; } setSignal(isDetect ? 1 : 0); // 出力をリスナーに送信 if (mAnalyzerEventListener != null) { mAnalyzerEventListener.onSignalPower(power); } } @Override public void onMarkerReached(AudioRecord recorder) { } }); return audioRecord; } public void setAnalyzerEventListener(AnalyzerEventListener listener) { mAnalyzerEventListener = listener; } private void setSignal(int signal) { if (signal != 0 && signal != 1) { //invalid parameter signal = 1; } mSignalBuffer[mBufferOffset] = signal; if (mBufferOffset == mSignalBuffer.length -1) { // 基本周期分受信したら、一つの信号を生成する。 int oneSignal = createOneSignal(); new AnalyzerTask().execute(oneSignal); mBufferOffset = 0; } else { // 基本周期分信号をバッファに保存 mBufferOffset++; } } private int createOneSignal() { int highCount = 0; int basicRateCount = mSignalBuffer.length; int output = 0; highCount = 0; for (int i = 0; i < mSignalBuffer.length; i++) { if (mSignalBuffer[i] > 0) { highCount++; } } Log.d(TAG, "high rates = " + ((double)highCount/(double)basicRateCount) * 100 + "%"); // 基本周期内に存在するHIGH信号の割合から、基本周期ごとにHIGH/LOWを判定する if ((double)highCount/(double)basicRateCount >= (double)HIGH_DENSITY_THRESHOLD/100.0) { Log.d(TAG, "High signal"); output = 1; } else { Log.d(TAG, "Low signal"); output = 0; } return output; } } |
まとめ
今回は超音波送受信による簡単なデータ送受信の仕組みを作りましたが、いかがでしたでしょうか?うまく送受信はできていますがまだまだ以下のような課題があります。
■データ送信ライブラリで音波を発生させるとき、小さく「プチッ」というノイズが発生する
AudioTrackを使用して超音波を発生させた場合、信号「0」から信号「1」の切り替わりの際にこのノイズが発生してしまうようです。
■データ送受信の速度が低い
今回は紹介した動画では200msで1つの信号で検証しましたが、これでは大きなデータを送ろうとすると長時間かかってしまいます。速度を高めるには、さらに短い間隔で音波を受信する必要がありますが、そうすると今度は解析対象のバッファサイズが小さくなるため、FFTの精度が低くなってしまいます。そのため、データ受信に影響が出ないよう注意する必要があります。
今回は解決にまで至りませんでしたが、興味のある方はぜひとも挑戦してみてください。
次回は、いよいよ最終回です。今回作ったライブラリを使用して便利なアプリを作ってみたいと思います。お楽しみに!!
失礼します。
動画と同じようなアプリを作ろうと試みているのですがなかなかうまくいきません。
もし可能であればこのライブラリを使用して実装したコードを共有して頂きたいのですが可能でしょうか?
y4yovp