SPRESENSEの音響通信をいよいよ実機で試していきます。前回、データ送受信はFSKで行うことにしました。データ送信はSPRESENSEで2つの音を出力することで実現します。データ受信はSPRESENSEのマイク入力とFFTを使って検波することで実現します。

それらの動作を検証するため、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

FFTClass 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);
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)

FFTClass 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);
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





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

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



SONY SPRESENSE メインボード CXD5602PWBMAIN1

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



SONY SPRESENSE 拡張ボード CXD5602PWBEXT1

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