はじめに
音波で屋内位置測位とかやってみたいなー、でも超音波スピーカーとか買うのも面倒だなーと思いつつ、手をこまねいていましたが、どうやらAndroidのスピーカーとマイクでも割と高音域までカバーできることが分かったので、挑戦してみました。
ただ、夏休みの期間が足りず、超音波でASK(OOK)ができたあたりで、残念ながらタイムオーバーです。
つかったもの
- MPAndroidChart : 苦労せずきれいなグラフがかける。意外と準リアルタイム処理でもひきつらないことがわかった。すごい。
- AudioRecord : マイクからひろった音を随時処理できるやつ。サンプリングレート44.1kHzはだいたいの機種が対応している模様。
- AudioTrack : 波形データを音声としてスピーカーから発生させられる。送信は20kHzくらいが精一杯だった。非可聴域ギリギリ(耳が良いとちょっと聞こえる)
- FFT4g : 京大大浦先生のFFTパッケージ(のjava移植版)、インターフェースにちょっと癖があるがめちゃくちゃ速い。NASAもで使われているとか、いないとか。
画面のキャプチャ
上から時間波形、包絡線、周波数波形。SoundLocaterって名前なのに全然測位までできてないのはご愛嬌。
「いー」って言ってるところ
「うー」って言ってるところ
18kHzの超音波でASK変調してるやつ
- 自分のスピーカーで出して、自分のマイクで拾ってるのでめっちゃキレイ
- ほかのデバイスからの音でも、10mくらいの部屋の中だったら拾えた
- ほかのデバイスからの音だと、こんなにキレイにはいかない
やってみて
- 搬送波再生とか同期検波とかさすがにやりたくないのでASKにした
- けど、やっぱり距離減衰と見分けがつかないから所詮お遊びレベル
- FSKだと帯域いくつか使うし、PSKはAndroidで見分けつくかな、なかなかしんどそう
- 最近のAndroidすごい
- 一昔前の端末だと処理に耐えられずに数秒で死んだ(リアルタイムで描画しようとしたからだけど)
- nova lite 全然死なない。描画遅延もほとんど気にならない
今後
- 先頭検出をきっちりやりたい
- ASK以外の変調でなにかいいものないかな
- 簡易CSMA/CA的なプロトコル入れて、うまいこと避け合うようにしたい
実装
一部抜粋で。全体のせるほどの元気がありません _(:3」∠)_
受信部
重要なのは下記ふたつ。
- AudioRecordクラスを使う
- フーリエ変換(FFT)
逆フーリエ変換がうまくいかず、かなりハマりましたが、結局素直に fft.rdft(-1, FFTdata)
で良かったみたい。
あと、参考元になかったもので追加したのは、下記ふたつ。
- 所望の帯域だけ通過させるために、周波数領域で不要帯域を0に
- 非同期検波しようと思って包絡線へ変換した(検波まで至らなかった)
// AudioRecordの作成
audioRec = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLING_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, 2*bufSize);
audioRec.startRecording();
isRecording = true;
//フーリエ解析スレッド
fft = new Thread(new Runnable() {
@Override
public void run() {
byte buf[] = new byte[bufSize * 2];
while (isRecording) {
audioRec.read(buf, 0, buf.length);
//エンディアン変換
ByteBuffer bf = ByteBuffer.wrap(buf);
bf.order(ByteOrder.LITTLE_ENDIAN);
short[] s = new short[bufSize];
for (int i = bf.position(); i < bf.capacity() / 2; i++) {
s[i] = bf.getShort();
}
//FFTクラスの作成と値の引き渡し
FFT4g fft = new FFT4g(FFT_SIZE);
double[] FFTdata = new double[FFT_SIZE];
for (int i = 0; i < bufSize; i++) {
FFTdata[i] = (double) s[i];
}
fft.rdft(1, FFTdata);
// デシベルの計算
short[] dbfs = new short[FFT_SIZE / 2];
for (int i = 0; i < FFT_SIZE; i += 2) {
dbfs[i / 2] = (short) (
20 * Math.log10(
Math.sqrt( Math.pow(FFTdata[i], 2) + Math.pow(FFTdata[i + 1], 2) )
/dB_baseline
)
);
// ★★特定の帯域通過にするために周波数領域で不要帯域を0に
if ( width/2 < Math.abs(rxFreq-resol*i/2) ) {
FFTdata[i] = 0;
FFTdata[i+1] = 0;
}
}
// 逆FFT
fft.rdft(-1, FFTdata);
// ★★包絡線検出のために絶対値とって移動平均
short[] s2 = new short[bufSize];
for (int i=16; i<bufSize; i++) {
for (int j=0; j<16; j++) {
s2[i-16] += (Math.abs(FFTdata[i-j]) * 2.0 / FFT_SIZE) /16;
}
}
updateChart(mChartTime, s);
updateChart(mChartTime2, s2);
updateChart(mChartFreq, dbfs);
}
// 録音停止
audioRec.stop();
audioRec.release();
}
});
//スレッドのスタート
fft.start();
参考
おおよそ下記のまんまです。
FFTはこれ。
送信部
受信部と比べて、送信部の方が結構しんどかった。
大事なのは下記。
- AudioTrackクラスを使う
- ただの矩形パルスで変調するとノイズ発生するので時間領域でなます
工夫など。
- 先頭検出のために、preambleとして16bit分01の繰り返し先頭につけてる
- 搬送波は1100の矩形パルス
- 1パルス40msのASK(Amplitude-Shift Keying)
- 最初ただの矩形パルスにしていたら、所望帯域外にもかなり音が漏れてしまって、18kHz(非可聴域)でもノイズが聞こえてしまった
- やむなくレイズドコサインフィルタで端をなましてやったら、そんなに気にならないレベルになった
// 周波数設定
int durationPerSymbol = 40;
int samplesPerT = 4;
int samplesPerSymbol = (int)( (double)( freq * durationPerSymbol) / 1000.0 );
samplesPerSymbol = samplesPerT * (int) Math.ceil(samplesPerSymbol / samplesPerT);
int samplingRate = samplesPerT * freq;
// データ生成
Random rnd = new Random();
byte[] preamble = new byte[]{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0};
byte[] data = new byte[64-preamble.length];
for (int i=0; i<data.length; i++) {
data[i] = (byte)rnd.nextInt(2);
}
int length = preamble.length + data.length;
// 搬送波生成
byte[] samples = new byte[samplesPerSymbol*length];
for (int i=0; i<samplesPerSymbol/samplesPerT*length; i++) {
samples[samplesPerT * i + 0] = (byte) 0x70;
samples[samplesPerT * i + 1] = (byte) 0x70;
samples[samplesPerT * i + 2] = (byte) 0x00;
samples[samplesPerT * i + 3] = (byte) 0x00;
}
// AM変調(プリアンブル)
int rolloffSamples = samplesPerSymbol/8;
for (int i=0; i<preamble.length; i++) {
for (int j=0; j<samplesPerSymbol; j++) {
double factor = 1.0;
// レイズドコサインフィルタ
if (j<rolloffSamples) {
factor = (1 - Math.cos( Math.PI/(double)rolloffSamples * (double)j )) /2;
} else if (samplesPerSymbol-rolloffSamples<j) {
factor = (1 - Math.cos( Math.PI/(double)rolloffSamples * (double)(samplesPerSymbol-j) )) /2;
}
samples[samplesPerSymbol * i + j] = (byte)( (double)(samples[samplesPerSymbol * i + j] * preamble[i]) * factor );
}
}
// AM変調(データ部)
for (int i=0; i<data.length; i++) {
for (int j=0; j<samplesPerSymbol; j++) {
double factor = 1.0;
// レイズドコサインフィルタ
if (j<rolloffSamples) {
factor = (1 - Math.cos( Math.PI/(double)rolloffSamples * (double)j )) /2;
} else if (samplesPerSymbol-rolloffSamples<j) {
factor = (1 - Math.cos( Math.PI/(double)rolloffSamples * (double)(samplesPerSymbol-j) )) /2;
}
samples[samplesPerSymbol * (preamble.length+i) + j] = (byte)( (double)(samples[samplesPerSymbol * (preamble.length+i) + j] * data[i]) * factor );
}
}
// 10回繰り返し
final byte[] txsamples = new byte[10*samples.length];
for (int i=0; i<10; i++) {
for (int j=0; j<samples.length; j++) {
txsamples[samples.length*i+j] = samples[j];
}
}
// AudioTrackコンストラクタ
final AudioTrack mTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
samplingRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_8BIT,
txsamples.length,
AudioTrack.MODE_STATIC
);
// 再生完了のリスナー設定
mTrack.setNotificationMarkerPosition(txsamples.length);
mTrack.setPlaybackPositionUpdateListener(
new AudioTrack.OnPlaybackPositionUpdateListener() {
public void onPeriodicNotification(AudioTrack track) {}
public void onMarkerReached(AudioTrack track) {
if (track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
track.stop();
track.release();
track = null;
}
}
}
);
// 波形データの書き込みと再生
track = new Thread(new Runnable() {
@Override
public void run() {
mTrack.reloadStaticData();
mTrack.write(txsamples, 0, txsamples.length);
mTrack.play();
}
});
//スレッドのスタート
track.start();
参考
AudioTrackまわりはこの辺を参考にした。ほとんどそのまま。
お世話になったみなさま
デジタル通信のお勉強
再生関係
録音関係
FFT関係
- 大浦先生のFFTライブラリのJava移植版
- 上記ライブラリでハマった人たち