前回までに SPRESENSE で FSK によるループバック音響通信に成功しました。今回は一足飛びに PC と SPRESENSE 間の音響無線通信を実現したいと思います。PC側は PyAudio で送受信を実現しました!





さらに、今回は誤り率を大幅に改善し、さらにビットレートもあげてみました。まずはそこから説明したいと思います。



■ 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);
}
}



実際の動きは次のようになりました。前回に比べると格段に早くなりました。これからどんどん改善を入れて、ゆくゆくは画像を送信できるようにしたいところです。


SpresenseFSKDouble.mp4


ここまでは、まだループバックなのでまだ面白みがありません。いよいよ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()



実際の動きはこちら、しっかりデータ通信ができています。


FSK_PC2SPRESENSE_TEST.mp4


■ 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()



実際の動きはこちらです。こちらもしっかりと通信できました。


FSK_SPRESENSE_PC_TEST.mp4


■ 全体を通じて
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





C言語によるディジタル無線通信技術

  • 作者: 幸宏, 神谷
  • 出版社/メーカー: コロナ社
  • 発売日: 2010/11/22
  • メディア: 単行本



SONY SPRESENSE メインボード CXD5602PWBMAIN1

  • 出版社/メーカー: スプレッセンス(Spresense)
  • メディア: Tools & Hardware



SONY SPRESENSE 拡張ボード CXD5602PWBEXT1

  • 出版社/メーカー: スプレッセンス(Spresense)
  • メディア: Tools & Hardware