二月の電子工作「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
二月の電子工作「SPRESENSEで音響通信」ーFSKによるループバック・データ通信の実現ー [SPRESENSE]
前回は、SPRESENSEで音響ループバックを試してみました。ただ単に音を出してマイクで音を拾うというレベルです。今回はいよいよ、FSKによるループバック・データ通信に挑みます。
■ FSKによるデータ送信部(モジュレータ)を実装
まずは送信の実装です。今回も前回と同じく”MARK”を 2kHz、”SPACE”を 1kHz に設定します。データの送信は、シリアルコンソールから入力された文字を送信するようにしました。
今回、受信側はサンプリングレート 48 kHz で 256 tap のFFTを使おうと考えているので、データ判定のために、1/48000 x 256 = 5.33msec 必要です。余裕をもって変調間隔は 40msec で試すことにしました。
いろいろ試行錯誤したのですが、どうも Oscillator ライブラリは 10msecスリープ毎に active() 関数を呼ぶとFIFOのオーバーフローが起きなさそう。(※ハマりポイント その1)
処理時間を測ると、一つの active() とスリープで 20msec程度かかっている模様。で、普通ループで処理をまわそうと思いますよね、、、でも、なぜか処理時間がばらつきます。ここのコードです。
送信間隔を測ってみると、active関数とusleep関数を直接二度呼んだ場合は問題ありませんが、for ループでは処理時間がばらばら。なんで???(※ハマりポイント その2)
実際に、受信側にどれだけ影響があるか確認するために、"0" と "1" が綺麗にならぶ大文字 'U' を送信し、FFTの結果を表示しててみました。
※acitve関数を2回コール(正常)
ご覧のようにループをすると結果がおかしくなり、使い物になりません。うーん、なんでだろう?ちょっと奥が深そうなので、ここは深追いせずに実装を優先します。
※for loop で active関数を2回コール(異常)
少し格好悪いですがactive関数を二重に呼ぶようにようにしてとりあえず実装完了としました。
せっかくなので、いろいろなデータを入力したときのFFTの出力波形をキャプチャしてみました。こういう絵面って、見てるだけでも飽きませんよね…。
(ところで、SSブログって動画アップロードできたんですね。、今更知った… ̄▽ ̄;)
再生できない場合、ダウンロードは🎥こちら
余談ですが、この実験を通して大文字”U"が大好きになりました。データ処理システムを作る人にとって、はなくてはならない相棒ですね。^^
■ FSKのデータ受信部(デモジュレータ)を実装
データ受信部はサンプリング周波数 48 kHz で、FFTが 256 tap の 5.3msec で単位で処理を行います。どのようににしてビット情報をフェッチするか色々と悩んだのですが、極めてハードウェア的なアプローチで行うことにしました。
スタートビットからカウンターを回して、ある間隔ごとにデータをフェッチするという方法です。(この結論に辿り着くまでに半日かかった…シンプル・イズ・ザ・ベスト!)
それに加えて、データ処理にステートマシンがあったほうが設計が楽になるので、簡単なステートマシンも設計しました。
実装は送信側もあるのでかなり長くなってしまっていますが晒しておきます。ライブラリ化したらGitHub にでも置こうと思います。
さて、デバッグ出力で動作も念入りに確認し、問題なさそうです。いよいよ入魂の”HELLO WORLD”!
再生できない場合、ダウンロードは🎥こちら
むちゃくちゃ遅いですけど動きましたねー。ちょっと感動です。やっぱり通信ができるって楽しいですね。せっかくなので、SPRESENSEの”HELLO WORLD”の声を録音しておきました。
さて、次はループバックから抜け出していよいよSPRESENSE同士でおしゃべりさせてみようかな。
^^
関連リンク
二月の電子工作「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で音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
■ FSKによるデータ送信部(モジュレータ)を実装
まずは送信の実装です。今回も前回と同じく”MARK”を 2kHz、”SPACE”を 1kHz に設定します。データの送信は、シリアルコンソールから入力された文字を送信するようにしました。
今回、受信側はサンプリングレート 48 kHz で 256 tap のFFTを使おうと考えているので、データ判定のために、1/48000 x 256 = 5.33msec 必要です。余裕をもって変調間隔は 40msec で試すことにしました。
いろいろ試行錯誤したのですが、どうも Oscillator ライブラリは 10msecスリープ毎に active() 関数を呼ぶとFIFOのオーバーフローが起きなさそう。(※ハマりポイント その1)
処理時間を測ると、一つの active() とスリープで 20msec程度かかっている模様。で、普通ループで処理をまわそうと思いますよね、、、でも、なぜか処理時間がばらつきます。ここのコードです。
uint32_t last_time = 0; void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); #if 0 for (int i = 0; i < repeat; ++i) { active(); usleep(ACTIVE_DURATION); } #else // interim measures active(); usleep(ACTIVE_DURATION); active(); usleep(ACTIVE_DURATION); #endif #if 0 uint32_t cur_time = millis(); Serial.println(String(cur_time - last_time)); last_time = cur_time; #endif }
送信間隔を測ってみると、active関数とusleep関数を直接二度呼んだ場合は問題ありませんが、for ループでは処理時間がばらばら。なんで???(※ハマりポイント その2)
実際に、受信側にどれだけ影響があるか確認するために、"0" と "1" が綺麗にならぶ大文字 'U' を送信し、FFTの結果を表示しててみました。
※acitve関数を2回コール(正常)
ご覧のようにループをすると結果がおかしくなり、使い物になりません。うーん、なんでだろう?ちょっと奥が深そうなので、ここは深追いせずに実装を優先します。
※for loop で active関数を2回コール(異常)
少し格好悪いですがactive関数を二重に呼ぶようにようにしてとりあえず実装完了としました。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (1000) #define FSK_CENTER (1500) #define MARK (2000) #define ACTIVE_DURATION (10000) /* usec */ #define SIGNAL_DURATION (2) /* x ACTIVE_DURATION */ Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; FFTClassFFT; 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 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; } 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); uint8_t sbit; if (peakFs > FSK_CENTER) { /* MARK */ sbit = 0x01; } else if (peakFs <= FSK_CENTER) { /* SPACE */ sbit = 0x00; } Serial.println(sbit); } } 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->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(-160, 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); } uint32_t last_time = 0; void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); #if 0 for (int i = 0; i < repeat; ++i) { active(); usleep(ACTIVE_DURATION); } #else // interim measures active(); usleep(ACTIVE_DURATION); active(); usleep(ACTIVE_DURATION); #endif #if 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); } }
せっかくなので、いろいろなデータを入力したときのFFTの出力波形をキャプチャしてみました。こういう絵面って、見てるだけでも飽きませんよね…。
(ところで、SSブログって動画アップロードできたんですね。、今更知った… ̄▽ ̄;)
再生できない場合、ダウンロードは🎥こちら
余談ですが、この実験を通して大文字”U"が大好きになりました。データ処理システムを作る人にとって、はなくてはならない相棒ですね。^^
■ FSKのデータ受信部(デモジュレータ)を実装
データ受信部はサンプリング周波数 48 kHz で、FFTが 256 tap の 5.3msec で単位で処理を行います。どのようににしてビット情報をフェッチするか色々と悩んだのですが、極めてハードウェア的なアプローチで行うことにしました。
スタートビットからカウンターを回して、ある間隔ごとにデータをフェッチするという方法です。(この結論に辿り着くまでに半日かかった…シンプル・イズ・ザ・ベスト!)
それに加えて、データ処理にステートマシンがあったほうが設計が楽になるので、簡単なステートマシンも設計しました。
実装は送信側もあるのでかなり長くなってしまっていますが晒しておきます。ライブラリ化したらGitHub にでも置こうと思います。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (1000) #define FSK_CENTER (1500) #define MARK (2000) #define ACTIVE_DURATION (10000) /* usec */ #define SIGNAL_DURATION (2) /* x ACTIVE_DURATION */ #define IDLE_STATE (0) #define STARTBIT_STATE (1) #define BITREC_STATE (2) #define STOPBIT_STATE (3) #define FETCH_INTERVAL (8) #define MSBBIT_INDEX (7) // #define DEBUG_ENABLE Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; FFTClassFFT; 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; } // debug_print(sbit); 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); uint8_t sbit; if (peakFs > FSK_CENTER) { /* MARK */ sbit = 0x01; } else if (peakFs <= FSK_CENTER) { /* SPACE */ sbit = 0x00; } 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; } } } 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->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(-160, 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); } /* Actually, sending data */ uint32_t last_time = 0; void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); #if 0 int i = 0; do { active(); usleep(ACTIVE_DURATION); ++i; } while (i < repeat); #else // interim measures active(); usleep(ACTIVE_DURATION); active(); usleep(ACTIVE_DURATION); #endif #if 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); } }
さて、デバッグ出力で動作も念入りに確認し、問題なさそうです。いよいよ入魂の”HELLO WORLD”!
再生できない場合、ダウンロードは🎥こちら
むちゃくちゃ遅いですけど動きましたねー。ちょっと感動です。やっぱり通信ができるって楽しいですね。せっかくなので、SPRESENSEの”HELLO WORLD”の声を録音しておきました。
さて、次はループバックから抜け出していよいよSPRESENSE同士でおしゃべりさせてみようかな。
^^
関連リンク
二月の電子工作「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で音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
はじめてのモールス通信: アナログとデジタルのハーモニー (アマチュア無線運用シリーズ)
- 出版社/メーカー: CQ出版
- 発売日: 2011/09/01
- メディア: 単行本
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
二月の電子工作「SPRESENSEで音響通信」ー音響ループバックを試すー [SPRESENSE]
SPRESENSEの音響通信をいよいよ実機で試していきます。前回、データ送受信はFSKで行うことにしました。データ送信はSPRESENSEで2つの音を出力することで実現します。データ受信はSPRESENSEのマイク入力とFFTを使って検波することで実現します。
それらの動作を検証するため、SPRESENSE単体で音をループバックして試してみました。
■ SPRESENSEで2つの音を出力する
まずは単体動作での確認です。SPRESENSEで2つの音を出力させるために、TomonobuHayakawa氏作成の「AudioOscillator」を使って試してみます。サンプルコードを参考にスケッチを作成してみました。2つの音は分かりやすくするため「1kHz」と「2kHz」にしました。
SPRESENSEの出力を直接PCのマイク入力につなげて音を録音してみました。
PCでキャプチャすると想定通りの波形となっています。
PCで録音してみました。未知との遭遇っぽくていいですねぇ~(古いか)
■ SPRESENSEの音を受信してFFTをかける
PCの音声出力をSPRESENSEのマイク入力につないでFFTをかけてみます。FFTはSignalProcessingライブラリのサンプルを参考にして作ってみました。サンプルはマルチコアを使っていますが、ここではメインコアのみで処理しています。
PCで録音した音の出力をSPRESENSEのマイク入力につなげてFFTで検波できるか試してみます。
出力を見ると、それっぽい値を出しています。ただ矩形の長さが微妙に違うので周波数変調間隔とFFTの受信範囲の調整が必要そうです。(この辺りが難しそう)
■ SPRESENSEのループバックで単体送受信を実現
これで送信・受信の単体テストはうまく行ったのでいよいよSPRESENSEの出力とマイク入力をつなぎあわせてループバックさせます。
送信、受信のスケッチを合体してみました。録音はスレッドにしてみました。タイミング問題がでるかなと思ったのですが、usleepを使ってうまく動かせたようです。
Arduinoのプロッタ出力はこんな感じになります。地味な映像ですけど、ちょっと感動。
あとは変調間隔と受信間隔の調整でなんとかデータを送受信ができそうです。
(^^)/~
関連リンク
二月の電子工作「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で音響通信」ーFSKによるループバック・データ通信の実現ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-22
二月の電子工作「SPRESENSEで音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
それらの動作を検証するため、SPRESENSE単体で音をループバックして試してみました。
■ SPRESENSEで2つの音を出力する
まずは単体動作での確認です。SPRESENSEで2つの音を出力させるために、TomonobuHayakawa氏作成の「AudioOscillator」を使って試してみます。サンプルコードを参考にスケッチを作成してみました。2つの音は分かりやすくするため「1kHz」と「2kHz」にしました。
#include <OutputMixer.h> #include <MemoryUtil.h> #include <arch/board/board.h> #include <AudioOscillator.h> #define MARK (2000) #define SPACE (1000) Oscillator *theOscillator; OutputMixer *theMixer; 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; } static bool active() { const uint32_t sample = 480; AsPcmDataParam pcm; int er; for (int i = 0; i < 5; i++) { er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) { Serial.println("PCM memory allocate error"); return false; } theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); /* Set PCM parameters */ 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; /* Send PCM */ 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_PLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); theOscillator->set(0,700,200,50,400); /* attack=1000,decay=700, sustain=30, release=300 */ 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(-160, 0, 0); board_external_amp_mute_control(false); /* Unmute */ Serial.println("Start Oscillator"); } void loop() { theOscillator->set(0, MARK); active(); usleep(10000); /* 10 msec */ theOscillator->set(0, SPACE); active(); usleep(10000); /* 10 msec */ }
SPRESENSEの出力を直接PCのマイク入力につなげて音を録音してみました。
PCでキャプチャすると想定通りの波形となっています。
PCで録音してみました。未知との遭遇っぽくていいですねぇ~(古いか)
■ SPRESENSEの音を受信してFFTをかける
PCの音声出力をSPRESENSEのマイク入力につないでFFTをかけてみます。FFTはSignalProcessingライブラリのサンプルを参考にして作ってみました。サンプルはマルチコアを使っていますが、ここではメインコアのみで処理しています。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ MediaRecorder *theRecorder; bool err_cb = false; #define FFT_LEN 256 #define CHANNEL_NUM 1 FFTClassFFT; 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); static uint8_t s_buffer[buffer_size*2]; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { Serial.println("Attention!"); if (p->error_code >= AS_ATTENTION_CODE_WARNING) { err_cb = true; } } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); 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("Start Recording"); } void loop() { uint32_t read_size; int err; err = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (err != MEDIARECORDER_ECODE_OK && err != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); exit(1); } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); return; } 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); Serial.println(String(peakFs)); }
PCで録音した音の出力をSPRESENSEのマイク入力につなげてFFTで検波できるか試してみます。
出力を見ると、それっぽい値を出しています。ただ矩形の長さが微妙に違うので周波数変調間隔とFFTの受信範囲の調整が必要そうです。(この辺りが難しそう)
■ SPRESENSEのループバックで単体送受信を実現
これで送信・受信の単体テストはうまく行ったのでいよいよSPRESENSEの出力とマイク入力をつなぎあわせてループバックさせます。
送信、受信のスケッチを合体してみました。録音はスレッドにしてみました。タイミング問題がでるかなと思ったのですが、usleepを使ってうまく動かせたようです。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; bool err_cb = false; #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (1000) #define MARK (2000) FFTClassFFT; 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); static uint8_t s_buffer[buffer_size*2]; 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) { Serial.println("Attention!"); if (p->error_code >= AS_ATTENTION_CODE_WARNING) { err_cb = true; } } 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; } 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); Serial.println(String(peakFs)); } } 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->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(-160, 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("Recording Start!"); //Serial.println("rec wait usec: " + String(rec_wait_usec)); task_create("audio recording", 120, 1024, audioReadFrames, NULL); } #define DURATION (10000) /* usec */ void loop() { theOscillator->set(0, MARK); active(); usleep(DURATION); theOscillator->set(0, SPACE); active(); usleep(DURATION); }
Arduinoのプロッタ出力はこんな感じになります。地味な映像ですけど、ちょっと感動。
あとは変調間隔と受信間隔の調整でなんとかデータを送受信ができそうです。
(^^)/~
関連リンク
二月の電子工作「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で音響通信」ーFSKによるループバック・データ通信の実現ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-22
二月の電子工作「SPRESENSEで音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
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
二月の電子工作「SPRESENSEで音響通信」ー送受信の検討ー [SPRESENSE]
SPRESENSEの弱点である通信を音で実現しようと始めたこのプロジェクト。今回は、その基本方針をたててみました。
■ 通信方法はFSKを採用
通信方法は周波数変調方式のFSKを採用します。FSKは次のように二つの周波数をそれぞれ"0"と"1"に割り当てて、データを送信する方法です。"1"に相当する周波数を"mark"、"0"に相当する周波数を"space"と呼ばれています。
データを送信するには、SPRESENSEからある特定の二つの周波数を生成し出力しなければなりません。BeatBoxのように、正弦波のMP3データを読み込んで再生という方法もありますが、スピードが出そうにないし、そもそも技術的にダサい。。
■ 送信は "AudioOscillator" ライブラリを活用
世の中にはいろいろと達人がいるもので、TomonobuHayakawa氏がSPRESENSEで特定の周波数音を発生させる、"AudioOscillator"というArduinoライブラリをGitHubで提供してくれています。
実はこのライブラリは大分前から目をつけていて、そのうちシンセサイザーを作りたいなと思ったりしています。(ラズパイシンセサイザーかっこいいので、SPRESENSEでも実現したい~)
送信側はこのライブラリを使うことで特定の2つの周波数を生成できそうです。ライブラリの使い方などはまだ良くわかっていませんが、サンプルは簡単に動いたので期待が持てそうです。
■ 受信間隔は周波数変調間隔の1/2以下
データの受信は、マイク入力で音を拾ってFFT解析でピーク周波数を検出することで "0:space" もしくは "1:mark" を検出するようにしたいと思います。でも、送信側の周波数の変調間隔と受信側の音のキャプチャ間隔はよく考える必要がありそう…。
簡単に考察すると、変調間隔と同じ "n msec" で受信間隔をとると、”0” と "1" の周波数が最悪50%ずつ混在してしまい ”mark/space" を判別できなくなりそうです。なので、受信間隔は最低でも "n/2 msec" である必要がありそうです。
これをもとに、送信側の変調間隔と受信側のサンプリング周波数とバッファ数など決めていきたいと思います。”AudioOscillator” と ”FFT” がうまく共存できれば、ループバックで検証ができるところまで行けるかも?
とりあえず、今日はここまで~。
(^^)/~
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「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で音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
■ 通信方法はFSKを採用
通信方法は周波数変調方式のFSKを採用します。FSKは次のように二つの周波数をそれぞれ"0"と"1"に割り当てて、データを送信する方法です。"1"に相当する周波数を"mark"、"0"に相当する周波数を"space"と呼ばれています。
データを送信するには、SPRESENSEからある特定の二つの周波数を生成し出力しなければなりません。BeatBoxのように、正弦波のMP3データを読み込んで再生という方法もありますが、スピードが出そうにないし、そもそも技術的にダサい。。
■ 送信は "AudioOscillator" ライブラリを活用
世の中にはいろいろと達人がいるもので、TomonobuHayakawa氏がSPRESENSEで特定の周波数音を発生させる、"AudioOscillator"というArduinoライブラリをGitHubで提供してくれています。
TomonobuHayakawa/Spresense-Playground Contribute to TomonobuHayakawa/Spresense-Playground development by creating an account on GitHub. github.com |
実はこのライブラリは大分前から目をつけていて、そのうちシンセサイザーを作りたいなと思ったりしています。(ラズパイシンセサイザーかっこいいので、SPRESENSEでも実現したい~)
送信側はこのライブラリを使うことで特定の2つの周波数を生成できそうです。ライブラリの使い方などはまだ良くわかっていませんが、サンプルは簡単に動いたので期待が持てそうです。
■ 受信間隔は周波数変調間隔の1/2以下
データの受信は、マイク入力で音を拾ってFFT解析でピーク周波数を検出することで "0:space" もしくは "1:mark" を検出するようにしたいと思います。でも、送信側の周波数の変調間隔と受信側の音のキャプチャ間隔はよく考える必要がありそう…。
簡単に考察すると、変調間隔と同じ "n msec" で受信間隔をとると、”0” と "1" の周波数が最悪50%ずつ混在してしまい ”mark/space" を判別できなくなりそうです。なので、受信間隔は最低でも "n/2 msec" である必要がありそうです。
これをもとに、送信側の変調間隔と受信側のサンプリング周波数とバッファ数など決めていきたいと思います。”AudioOscillator” と ”FFT” がうまく共存できれば、ループバックで検証ができるところまで行けるかも?
とりあえず、今日はここまで~。
(^^)/~
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「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で音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
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
二月の電子工作「SPRESENSEで音響通信」 [SPRESENSE]
SPRESENSEの最大の弱点は何かというと通信機能!BLEやWiFiのアドオンボードはたくさんあるけど、できれば追加投資なしで通信ができたら…。
ということで、今月はできるかどうか分かりませんが、SPRESENSEのメインボードと拡張ボードだけで通信を実現したいと思います。せっかく音が得意なSPRESENSEなので、FSK通信にチャレンジ!(まぢ出来るの??)
通信方式は、非同期シリアル通信で一般的に使われている、”パリティなし”、”データ8ビット”、”ストップビット1ビット”の一択限定です。
ボーレート(通信速度)はどれくらい出るか想像もできないので、出来高払いということで。通信相手はどうしようかなぁ。宣言したはいいものの、今月中にできるかどうか無茶苦茶不安。。。
(。-`ω´-)
関連リンク
二月の電子工作「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で音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
ということで、今月はできるかどうか分かりませんが、SPRESENSEのメインボードと拡張ボードだけで通信を実現したいと思います。せっかく音が得意なSPRESENSEなので、FSK通信にチャレンジ!(まぢ出来るの??)
通信方式は、非同期シリアル通信で一般的に使われている、”パリティなし”、”データ8ビット”、”ストップビット1ビット”の一択限定です。
ボーレート(通信速度)はどれくらい出るか想像もできないので、出来高払いということで。通信相手はどうしようかなぁ。宣言したはいいものの、今月中にできるかどうか無茶苦茶不安。。。
(。-`ω´-)
関連リンク
二月の電子工作「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で音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
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