二月の電子工作「SPRESENSEで音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー [SPRESENSE]
前回までに SPRESENSE で FSK によるループバック音響通信に成功しました。今回は一足飛びに PC と SPRESENSE 間の音響無線通信を実現したいと思います。PC側は PyAudio で送受信を実現しました!
さらに、今回は誤り率を大幅に改善し、さらにビットレートもあげてみました。まずはそこから説明したいと思います。
■ SPACE/MARKの周波数判定の精度をあげる
前回の "SPACE/MARK" の判定は、中心周波数よりピークが上だったら "MARK"、下だったら "SPCE" という非常にざっくりとしたもの。
これでは雑音を拾いまくりです。特に生活雑音は低周波に大きな成分をもつので、これはマズイ。なんともお恥ずかしいコードです。なので、ピンポイントに周波数を見るようにしました。
48kHz の 256 tap で FFTをしているので、周波数分解能は187.5Hz。ですので、その整数倍を "MARK" と "SPACE" に割り当て、その近辺のパワースペクトルを見れば精度よく判定できそうです。
低周波はノイズのパワーが大きいので避けたいのと、可聴領域で実験していると耳にやさしくないので、負担のかからない 10kHz 以上にしました。("SPACE"=13125 hz, "MARK"=16875 hz)
これで、誤り率はは格段に減らすことができました。
■ ビットレートを 25 bps から 50 bps に倍速にする
前回はテストということもあり、安全を見て 25bps にしました。安定して動かすことができるようになったので2倍の 50bps にあげることにしました。取りこぼしをなくすため、変調間隔を FFT 解析周期 5.3msec の整数倍(4倍)の 21.2msec に調整し、フェッチング間隔を4回にしています。
■ 音響無線通信をループバックで実現する
次は、音響無線通信をループバックで試してみたいと思います。
”SPACE/MARK の判定方法の改善” と、”ビットレート改善” を盛り込んだコードは次のようになりました。デバッグコードなどもあり、少し長めなのはご容赦ください。
実際の動きは次のようになりました。前回に比べると格段に早くなりました。これからどんどん改善を入れて、ゆくゆくは画像を送信できるようにしたいところです。
再生できない場合、ダウンロードは🎥こちら
ここまでは、まだループバックなのでまだ面白みがありません。いよいよPCとの通信にチャレンジしたいと思います。
■ PyAudio を使って PC から SPRESENSE へデータ送信!
当初、SPRESENSE 同士で通信ということも考えたのですが、SPRESENSE 同士で通信しても面白みはないので、PC とデータ通信することにしました。手始めに PC から SPRESENSE へデータを送信してみることにしました。
PC 側はいろいろと考えたのですが、Raspberry Pi との通信も考慮して PyAudio を使うことにしました。ひさしぶりの Python プログラミングなので色々と苦労しましけど。(詳しくはTwitterへ!)
PC 側の Python コードです。ちなみに、PyAudio は Python 3.6 しか正式サポートされていないので、Anaconda で 3.6 の仮想環境を使って実装しています。
実際の動きはこちら、しっかりデータ通信ができています。
再生できない場合、ダウンロードは🎥こちら
■ PyAudio を使って、PC で SPRESENSE からの音響FSK無線通信を受けてみる
次は SPRESENSE からの無線通信を PC で受信します。PC にピンマイクをつなげて受信してみます。
SRESENSE の受信ルーチンをそのまま Python コードに置き直しただけのコードで試してみました。受信も PyAudio を使っています。
実際の動きはこちらです。こちらもしっかりと通信できました。
再生できない場合、ダウンロードは🎥こちら
■ 全体を通じて
PCーSPRESENSE 間を音響無線通信を実現することができました。まだ 50bps と低いレートですが、まだまだ通信速度をあげる余地はありそうです。無線の場合、だいたい 2m くらいまでは通信ができそうですが、安定しているのは 1m ちょいといったところです。
あと大事なのはスピーカーとマイク。こちらはデバイスによってはまったく通信が出来ないので注意が必要です。ノートPC のペラッペラのスピーカーでは誤り率が格段にあがりますし、マイクもモノによっては駄目なものもあります。
特に中華製の100円マイクは品質がばらばらで同じ製品でも誤り率に大きな差が出ていました。一方、スピーカーは低音のきちんと出るものであれば Bluetooth スピーカーでも問題ありませんでした。
低速ですが無線通信ができると楽しいものですね。そのうち、AFSK や PSK にもチャレンジしてみようかな!
(^^)/~
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「SPRESENSEで音響通信」ー送受信の検討ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-07
二月の電子工作「SPRESENSEで音響通信」ー音響ループバックを試すー
https://makers-with-myson.blog.ss-blog.jp/2021-02-14
二月の電子工作「SPRESENSEで音響通信」ーFSKによるループバック・データ通信の実現ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-22
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
さらに、今回は誤り率を大幅に改善し、さらにビットレートもあげてみました。まずはそこから説明したいと思います。
■ SPACE/MARKの周波数判定の精度をあげる
前回の "SPACE/MARK" の判定は、中心周波数よりピークが上だったら "MARK"、下だったら "SPCE" という非常にざっくりとしたもの。
uint8_t sbit; if (peakFs > FSK_CENTER) { // MARK sbit = 0x01; } else if (peakFs <= FSK_CENTER) { // SPACE sbit = 0x00; }
これでは雑音を拾いまくりです。特に生活雑音は低周波に大きな成分をもつので、これはマズイ。なんともお恥ずかしいコードです。なので、ピンポイントに周波数を見るようにしました。
48kHz の 256 tap で FFTをしているので、周波数分解能は187.5Hz。ですので、その整数倍を "MARK" と "SPACE" に割り当て、その近辺のパワースペクトルを見れば精度よく判定できそうです。
低周波はノイズのパワーが大きいので避けたいのと、可聴領域で実験していると耳にやさしくないので、負担のかからない 10kHz 以上にしました。("SPACE"=13125 hz, "MARK"=16875 hz)
これで、誤り率はは格段に減らすことができました。
■ ビットレートを 25 bps から 50 bps に倍速にする
前回はテストということもあり、安全を見て 25bps にしました。安定して動かすことができるようになったので2倍の 50bps にあげることにしました。取りこぼしをなくすため、変調間隔を FFT 解析周期 5.3msec の整数倍(4倍)の 21.2msec に調整し、フェッチング間隔を4回にしています。
■ 音響無線通信をループバックで実現する
次は、音響無線通信をループバックで試してみたいと思います。
”SPACE/MARK の判定方法の改善” と、”ビットレート改善” を盛り込んだコードは次のようになりました。デバッグコードなどもあり、少し長めなのはご容赦ください。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN (200) /* default +0dB */ #define SPEAKER_VOLUME (-160) /* default -160 */ #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (13125) #define MARK (16875) #define ACTIVE_DURATION (10000) /* usec */ #define IDLE_STATE (0) #define STARTBIT_STATE (1) #define BITREC_STATE (2) #define STOPBIT_STATE (3) #define FETCH_INTERVAL (4) #define MSBBIT_INDEX (7) // #define DEBUG_ENABLE Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; FFTClass<CHANNEL_NUM, FFT_LEN> FFT; static float pDst[FFT_LEN]; static const uint32_t rec_bitrate = AS_BITRATE_48000; static const uint32_t rec_sampling_rate = AS_SAMPLINGRATE_48000; static const uint8_t rec_channel_num = CHANNEL_NUM; static const uint8_t rec_bit_length = AS_BITLENGTH_16; static const int32_t buffer_size = FFT_LEN * sizeof(int16_t); static const uint32_t rec_wait_usec = buffer_size * (1000000 / rec_sampling_rate) / 2; static uint8_t s_buffer[buffer_size*2]; static uint8_t frame_cnt = 0; static uint8_t fetch_timing = 1; static uint8_t bpos = 0; static uint8_t cur_state = IDLE_STATE; static char output = 0; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { // printf("mp cb %x %x %x\n", e, r1, r2); return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { if (p->error_code >= AS_ATTENTION_CODE_WARNING) { Serial.println("Attention!"); } } static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) { // Serial.println("mixer done callback"); return; } static void mixer_send_cb(int32_t id, bool is_end) { // Serial.println("mixer send callback"); return; } void debug_print(uint8_t sbit) { #ifdef DEBUG_ENABLE static bool first_print = true; if (first_print) { Serial.println("state, sbit, bpos, fcnt"); first_print = false; } Serial.print(String(cur_state)); Serial.print("," + String(sbit)); Serial.print("," + String(bpos)); Serial.print("," + String(frame_cnt)); Serial.println(); #endif } void idle_phase(uint8_t sbit) { if (sbit == 0) { cur_state = STARTBIT_STATE; } frame_cnt = 0; fetch_timing = 1; output = 0; return; } void startbit_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); cur_state = BITREC_STATE; fetch_timing += FETCH_INTERVAL; return; } void bitrec_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); output = output | (sbit << bpos); fetch_timing += FETCH_INTERVAL; if (++bpos > MSBBIT_INDEX) { cur_state = STOPBIT_STATE; } return; } bool stopbit_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); Serial.write(output); // interim implementation frame_cnt = 0; bpos = 0; cur_state = IDLE_STATE; return; } static void audioReadFrames() { uint32_t read_size; int er; while (true) { er = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (er != MEDIARECORDER_ECODE_OK && er != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); break; } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); continue; } FFT.put((q15_t*)s_buffer, FFT_LEN); FFT.get(pDst, 0); uint32_t index; float maxValue; int max_line = FFT_LEN/2.56; arm_max_f32(pDst, max_line, &maxValue, &index); float peakFs = index * (rec_sampling_rate / FFT_LEN); const float fc = (SPACE + MARK) / 2; float Space = 0.5*pDst[69] + pDst[70] + pDst[71] + 0.5*pDst[72]; float Mark = 0.5*pDst[89] + pDst[90] + pDst[91] + 0.5*pDst[92]; uint8_t sbit; if (peakFs < fc && Space > 0.5 && Space > Mark) sbit = 0; else if (peakFs > fc && Mark > 0.5 && Mark > Space) sbit = 1; else sbit = 1; // no signal noise detected. #if 0 Serial.print(String(Space,3) + ", " + String(Mark, 3) + ", "); Serial.println(sbit); #else switch(cur_state) { case IDLE_STATE: idle_phase(sbit); break; case STARTBIT_STATE: startbit_phase(sbit); break; case BITREC_STATE: bitrec_phase(sbit); break; case STOPBIT_STATE: stopbit_phase(sbit); break; } #endif } } static bool active() { const uint32_t sample = 480; int er; for (int i = 0; i < 5; i++) { AsPcmDataParam pcm; /* get PCM */ er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) break; theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); pcm.identifier = 0; pcm.callback = 0; pcm.bit_length = 16; pcm.size = sample*2*2; pcm.sample = sample; pcm.is_end = false; pcm.is_valid = true; er = theMixer->sendData(OutputMixer0, mixer_send_cb, pcm); if (er != OUTPUTMIXER_ECODE_OK) { Serial.println("OutputMixer send error: " + String(er)); return false; } } return true; } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); // theOscillator->set(0, 700, 200, 50, 400); // attack=0, decay=700, sustain=50, release=400 theOscillator->set(10, 10, 100, 50, 10); // attack=0, decay=700, sustain=50, release=400 theOscillator->lfo(0, 4, 2); theMixer = OutputMixer::getInstance(); theMixer->activateBaseband(); theMixer->create(); theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); theMixer->activate(OutputMixer0, HPOutputDevice, mixer_done_cb); theMixer->setVolume(SPEAKER_VOLUME, 0, 0); board_external_amp_mute_control(false); /* Unmute */ FFT.begin(WindowRectangle, rec_channel_num, 0); theRecorder = MediaRecorder::getInstance(); theRecorder->begin(rec_attention_cb); theRecorder->setCapturingClkMode(MEDIARECORDER_CAPCLK_NORMAL); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, rec_done_cb); theRecorder->init(AS_CODECTYPE_LPCM, rec_channel_num , rec_sampling_rate, rec_bit_length, rec_bitrate, "/mnt/sd0/BIN"); theRecorder->setMicGain(ANALOG_MIC_GAIN); theRecorder->start(); // Serial.println("rec wait usec: " + String(rec_wait_usec)); task_create("audio recording", 120, 1024, audioReadFrames, NULL); } void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); // interim measures active(); usleep(ACTIVE_DURATION); // I'm not sure why usleep takes 20msec? delayMicroseconds(1200); // adjustment to fit FFT period #if 0 static uint32_t last_time = 0; uint32_t cur_time = millis(); Serial.println(String(cur_time - last_time)); last_time = cur_time; #endif } void send_char(uint8_t c, int n = SIGNAL_DURATION) { send_signal(SPACE, n); // send start bit for (int n = 0; n < 8; ++n, c = c >> 1) { /* LSB fast */ if (c & 0x01) { /* mark (1) */ send_signal(MARK, n); } else { /* space (0) */ send_signal(SPACE, n); } } send_signal(MARK, n); // send stop bit } void loop() { send_signal(MARK); // default level is mark if (Serial.available()) { char c = Serial.read(); send_char(c); } }
実際の動きは次のようになりました。前回に比べると格段に早くなりました。これからどんどん改善を入れて、ゆくゆくは画像を送信できるようにしたいところです。
再生できない場合、ダウンロードは🎥こちら
ここまでは、まだループバックなのでまだ面白みがありません。いよいよPCとの通信にチャレンジしたいと思います。
■ PyAudio を使って PC から SPRESENSE へデータ送信!
当初、SPRESENSE 同士で通信ということも考えたのですが、SPRESENSE 同士で通信しても面白みはないので、PC とデータ通信することにしました。手始めに PC から SPRESENSE へデータを送信してみることにしました。
PC 側はいろいろと考えたのですが、Raspberry Pi との通信も考慮して PyAudio を使うことにしました。ひさしぶりの Python プログラミングなので色々と苦労しましけど。(詳しくはTwitterへ!)
PC 側の Python コードです。ちなみに、PyAudio は Python 3.6 しか正式サポートされていないので、Anaconda で 3.6 の仮想環境を使って実装しています。
import wave import struct import numpy as np import pyaudio import pandas as pd from matplotlib.pylab import * def createSineWave (A, f0, fs, length): # A: Amplitude # f0: Frequency # f1: Frequency # fs: Sampling rate # length: Playing time " data = [] # Genrating sinwave ranging [-1.0, 1.0] for n in np.arange(length * fs): s = A * (np.sin(2 * np.pi * f0 * n / fs)) # Clipping if s > 1.0: s = 1.0 if s < -1.0: s = -1.0 data.append(s) # Tasnsforming the sinwave in range of integer [-32768, 32767] data = [int(x * 32767.0) for x in data] # Trasforming into binary data data = struct.pack("h" * len(data), *data) return data def sendSignal(freq, time): data = createSineWave(amp, freq, fs, time) length = len(data) sp = 0 chunk = 1024 buffer = data[sp:sp+chunk] while sp < length : stream.write(buffer) sp = sp + chunk buffer = data[sp:sp+chunk] return if __name__ == "__main__" : while True: try: sdata = input() sdata = sdata + "\n" length = 100 n = 0 fs = 48000 # sampling rate mark = 16875 # Hz space = 13125 # Hz amp = 1.0 p = pyaudio.PyAudio() stream = p.open(format=pyaudio.paInt16, channels=1, rate=int(fs), output= True) # ready to send signal sendSignal(mark, 0.1) mod_period = 0.0212 for c in sdata: print(c) # start bit 40msec sendSignal(space, mod_period) ic = ord(c) for x in range(8): if (ic & 0x01): sendSignal(mark, mod_period) else: sendSignal(space, mod_period) ic = ic >> 1 # stop bit sendSignal(mark, mod_period) # finish sending signal sendSignal(mark, 0.1) except KeyboardInterrupt: break stream.close() p.terminate()
実際の動きはこちら、しっかりデータ通信ができています。
再生できない場合、ダウンロードは🎥こちら
■ PyAudio を使って、PC で SPRESENSE からの音響FSK無線通信を受けてみる
次は SPRESENSE からの無線通信を PC で受信します。PC にピンマイクをつなげて受信してみます。
SRESENSE の受信ルーチンをそのまま Python コードに置き直しただけのコードで試してみました。受信も PyAudio を使っています。
import pyaudio import numpy as np import time from scipy.signal import argrelmax from matplotlib import pyplot as plt CHUNK = 256 RATE = 48000 # sampling rate dt = 1/RATE freq = np.linspace(0,1.0/dt,CHUNK) MARK = 16875 SPACE = 13125 fc = (MARK + SPACE) / 2 IDLE_STATE = 0 STARTBIT_STATE = 1 BITREC_STATE = 2 STOPBIT_STATE = 3 class StateMachine(): FETCH_INTERVAL = 4 MSBBIT_INDEX = 7 cur_state = IDLE_STATE frame_cnt = 0 fetch_timing = 1 output = 0 bpos = 0 def __init__(self): print("Start RX") def state(self): return self.cur_state def debug_print(self, sbit): return print(self.cur_state, end=',') print( sbit, end=',') print(self.bpos, end=',') print(self.frame_cnt) def idle_phase(self, sbit): self.frame_cnt = 0 self.fetch_timing = 1 self.output = 0 if (sbit == 0): self.cur_state = STARTBIT_STATE def startbit_phase(self, sbit): self.frame_cnt = self.frame_cnt + 1 if (self.frame_cnt != self.fetch_timing): return self.debug_print(sbit) self.bpos = 0 self.cur_state = BITREC_STATE self.fetch_timing = self.fetch_timing + self.FETCH_INTERVAL def bitrec_phase(self, sbit): self.frame_cnt = self.frame_cnt + 1 if (self.frame_cnt != self.fetch_timing): return self.debug_print(sbit) self.output = self.output | (sbit << self.bpos) self.fetch_timing = self.fetch_timing + self.FETCH_INTERVAL self.bpos = self.bpos + 1 if (self.bpos > self.MSBBIT_INDEX): self.cur_state = STOPBIT_STATE def stopbit_phase(self, sbit): self.frame_cnt = self.frame_cnt + 1 if (self.frame_cnt != self.fetch_timing): return self.debug_print(sbit) coutput = chr(self.output) print(coutput, end="") self.frame_cnt = 0 self.bpos = 0 self.cur_state = IDLE_STATE if __name__=='__main__': p = pyaudio.PyAudio() stream = p.open(format=pyaudio.paInt16 , channels=1 , rate=RATE , frames_per_buffer=CHUNK , input=True , output=False) state = StateMachine() while stream.is_active(): try: input = stream.read(CHUNK, exception_on_overflow=False) ndarray = np.frombuffer(input, dtype='int16') ndarray = ndarray.astype(float)/float(2**15-1) f = np.fft.fft(ndarray) f_abs = np.abs(f) index = np.argmax(f_abs[:(int)(CHUNK/2)]) peak_f = index * (RATE / CHUNK) #print(peak_f) space = 0.5*f_abs[69] + f_abs[70] + f_abs[71] + 0.5*f_abs[72] mark = 0.5*f_abs[89] + f_abs[90] + f_abs[91] + 0.5*f_abs[92] sbit = 1 if (peak_f < fc and space > 1.0 and space > mark): #print("Space") sbit = 0 elif (peak_f > fc and mark > 1.0 and mark > space): #print("Mark") sbit = 1 if (state.state() == IDLE_STATE): state.idle_phase(sbit) elif (state.state() == STARTBIT_STATE): state.startbit_phase(sbit) elif (state.state() == BITREC_STATE): state.bitrec_phase(sbit) elif (state.state() == STOPBIT_STATE): state.stopbit_phase(sbit) except KeyboardInterrupt: break stream.stop_stream() stream.close() P.terminate()
実際の動きはこちらです。こちらもしっかりと通信できました。
再生できない場合、ダウンロードは🎥こちら
■ 全体を通じて
PCーSPRESENSE 間を音響無線通信を実現することができました。まだ 50bps と低いレートですが、まだまだ通信速度をあげる余地はありそうです。無線の場合、だいたい 2m くらいまでは通信ができそうですが、安定しているのは 1m ちょいといったところです。
あと大事なのはスピーカーとマイク。こちらはデバイスによってはまったく通信が出来ないので注意が必要です。ノートPC のペラッペラのスピーカーでは誤り率が格段にあがりますし、マイクもモノによっては駄目なものもあります。
特に中華製の100円マイクは品質がばらばらで同じ製品でも誤り率に大きな差が出ていました。一方、スピーカーは低音のきちんと出るものであれば Bluetooth スピーカーでも問題ありませんでした。
低速ですが無線通信ができると楽しいものですね。そのうち、AFSK や PSK にもチャレンジしてみようかな!
(^^)/~
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「SPRESENSEで音響通信」ー送受信の検討ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-07
二月の電子工作「SPRESENSEで音響通信」ー音響ループバックを試すー
https://makers-with-myson.blog.ss-blog.jp/2021-02-14
二月の電子工作「SPRESENSEで音響通信」ーFSKによるループバック・データ通信の実現ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-22
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
コメント 0