Processingを使って音響診断AIの準備しよう！ 

# 初めに
Spresenseは、音響診断に向いているとよく言われていますが、現時点で音響診断用のDNN のネットワークは画像処理ほど、豊富でなく自分自身でトライを繰り返す必要があります。
そこで、いきなり実機でのトライをするより、PC上で可視化しながら DNNネットワークを構築したいと思い、生波形の可視化とデータ記録を**Processing**で行なってみました！

以下の記事（Processing+Spresense）を参考にしています。

[**Spresenseカメラからのキャプチャ画像をリアルタイムにProcessingに転送してライブストリーミング再生する**
https://qiita.com/baggio/items/da2c5f01fac6ea57514f](https://qiita.com/baggio/items/da2c5f01fac6ea57514f)

# 用意したもの

[**SPRESENSEメインボード[CXD5602PWBMAIN1]**
https://www.switch-science.com/catalog/3900/](https://www.switch-science.com/catalog/3900/)
<img src="https://d2air1d4eqhwg2.cloudfront.net/images/3900/500x500/42ccd153-3e89-4f7c-921c-775bc7de996d.jpg" width="200">

[**SPRESENSE拡張ボード[CXD5602PWBEXT1]**
https://www.switch-science.com/products/3901](https://www.switch-science.com/products/3901)
<img src="https://www.switch-science.com/cdn/shop/products/5ecdbd33-8e31-4798-b31f-cedc64e19830_9ab348f5-b647-4d2a-bb57-fe96aaa54b10_500x500.jpg" width="200">

[**SPRESENSE Wi-Fi Add-onボード iS110B**
https://idy-design.com/product/is110b.html](
https://idy-design.com/product/is110b.html)
<img src="https://idy-design.com/wp-content/uploads/2019/02/460x230-iS110B_Side.png" width="300">

[**B-stem 4CM01 SPRESENSE用4chマイク基板**
https://www.switch-science.com/products/7996](
https://www.switch-science.com/products/7996)
<img src="https://www.switch-science.com/cdn/shop/products/557aa6a1-6438-4707-b221-687236b76b62_69451543-05f5-452e-a366-49473cb1d811_522x201.jpg?v=1693796122" width="300">

[**全方向性マイク カー外部マイク 高感度 コンデンサーマイク GPS DVDラジオ用の車の外部マイク- 2.5mm直角 黒**
https://www.amazon.co.jp/-/en/Omnidirectional-Microphone-External-Sensitivity-Condenser/dp/B08YYYJQCD](
https://www.amazon.co.jp/-/en/Omnidirectional-Microphone-External-Sensitivity-Condenser/dp/B08YYYJQCD)
<img src="https://m.media-amazon.com/images/I/61ECt2ehlZL._AC_SX522_.jpg" width="300">

# ブログ（元ネタ）
この記事は、以下のブログに過去に投稿したもののまとめです。

[音響診断AIを作るためにProcessingを使って準備しよう！（WiFi Core編）
http://spresense.livedoor.blog/archives/34136261.html](http://spresense.livedoor.blog/archives/34136261.html)

[音響診断AIを作るためにProcessingを使って準備しよう！（Main Core編）
http://spresense.livedoor.blog/archives/34144064.html](http://spresense.livedoor.blog/archives/34144064.html)

[音響診断AIを作るためにProcessingを使って準備しよう！（Processing編）
http://spresense.livedoor.blog/archives/34137779.html](http://spresense.livedoor.blog/archives/34137779.html)

# Spresense側のinoについて

Spresense側のサンプルコードについて記載します。

## MainCore（音声取得）inoについて

MainCoreでは、主に音声の取得を行います。
音声の生データを取得するサンプルは、ここにあるので、

[**1.3.6. PCM データを取り出す**
https://developer.sony.com/spresense/development-guides/arduino_tutorials_ja.html#_pcm_capture_objif](https://developer.sony.com/spresense/development-guides/arduino_tutorials_ja.html#_pcm_capture_objif)

このコードを改変するようにします。

ってことで、まずは変数定義ですが、

~~~ c
#include <FrontEnd.h>
#include <MemoryUtil.h>

FrontEnd *theFrontEnd;

// Audio parameters.
static const int32_t channel_num  = AS_CHANNEL_MONO;
static const int32_t bit_length   = AS_BITLENGTH_16;
static const int32_t frame_sample = 512;
static const int32_t frame_size   = frame_sample * (bit_length / 8) * channel_num;
static const int32_t mic_gain     = 160;

static CMN_SimpleFifoHandle simple_fifo_handle;
static const int32_t fifo_size  = frame_size * 20;
static uint32_t fifo_buffer[fifo_size / sizeof(uint32_t)];

static const int32_t proc_size  = frame_size;
static uint8_t proc_buffer[proc_size];

bool isEnd = false;
bool ErrEnd = false;

/* Multi-core parameters */
const int proc_core = 1;
const int conn_core = 2
~~~

まずは、シングルチャネルの16ビット、フレームサイズは512サンプル、バッファはのちの信号処理を鑑みて、10倍に増やしました。
信号処理用にコア1を予約しておいて、コア2で通信処理をします。

で、各Callbackは特に変更なしで、setupはMPライブラリの初期化などを追加します。

~~~ c
/**
 *  @brief Setup audio device to capture PCM stream
 *
 *  Select input device as microphone <br>
 *  Set PCM capture sapling rate parameters to 48 kb/s <br>
 *  Set channel number 4 to capture audio from 4 microphones simultaneously <br>
 */
char server_cid = 0;

void setup()
{
  Serial.begin(CONSOLE_BAUDRATE);

  /* Initialize memory pools and message libs */
  initMemoryPools();
  createStaticPools(MEM_LAYOUT_RECORDER);

  if (CMN_SimpleFifoInitialize(&simple_fifo_handle, fifo_buffer, fifo_size, NULL) != 0) {
    print_err("Fail to initialize simple FIFO.\n");
    exit(1);
  }
  
  /* Launch SubCore */
  ret = MP.begin(conn_core);
  if (ret < 0) {
    printf("MP.begin error = %d\n", ret);
  }

  /* receive with non-blocking */
  MP.RecvTimeout(1);

  /* start audio system */
  theFrontEnd = FrontEnd::getInstance();
  theFrontEnd->begin(frontend_attention_cb);

  puts("initialization FrontEnd");

  /* Set capture clock */
  theFrontEnd->setCapturingClkMode(FRONTEND_CAPCLK_NORMAL);

  /* Activate Objects. Set output device to Microphone */
  theFrontEnd->activate(frontend_done_callback);

  usleep(100 * 1000); /* waiting for Mic startup */

  /* Initialize of capture */
  AsDataDest dst;
  dst.cb = frontend_pcm_callback;

  theFrontEnd->init(channel_num,
                    bit_length,
                    frame_sample,
                    AsDataPathCallback,
                    dst);

  theFrontEnd->setMicGain(mic_gain);

  sleep(10);

  theFrontEnd->start();
  puts("Capturing Start!");

}
~~~

ここで、10秒待っていますが、これはWiFiの初期化待ちの時間で、SubCoreからちゃんと初期化完了通知をもらえば良いのですが、ここも、とりあえず適当に合わせてます。(^^;)

で、実際のフレーム処理の部分ですが、loopの中で呼ばれている、execute_aframe の中で処理しています。

~~~ c
void loop() {

  static Request request;
  
  /* Execute audio data */
  if (!execute_aframe()) {
    puts("Capturing Error!");
    ErrEnd = true;
  }

  if (isEnd) {
    theFrontEnd->stop();
    goto exitCapturing;
  }

  if (ErrEnd) {
    puts("Error End");
    theFrontEnd->stop();
    goto exitCapturing;
  }

  return;

exitCapturing:
  theFrontEnd->deactivate();
  theFrontEnd->end();

  puts("End Capturing");
  exit(1);

}
~~~

execute_aframe の中で、SimpleFIFOから取り出して、WiFiのSubCoreに送っています。

~~~ c
bool execute_aframe()
{
  int8_t sndid = 100; /* user-defined msgid */
  static Request request;

  size_t size = CMN_SimpleFifoGetOccupiedSize(&simple_fifo_handle);

  if (size > 0) {
    if (size > proc_size) {
      size = (size_t)proc_size;
    }

    if (CMN_SimpleFifoPoll(&simple_fifo_handle, (void*)proc_buffer, size) == 0) {
      printf("ERROR: Fail to get data from simple FIFO.\n");
      return false;
    }

  　static Request request;

    request.buffer   = proc_buffer;
    request.sample   = size / 2;
    request.frame_no = g_frame_no;
    g_frame_no++;
    MP.Send(sndid, &request, conn_core);
  }
  return true;
}
~~~

SubCoreに送るデータ形式は、以下になります。
この構造体は、Core間で共有して使います。

~~~ c
/* MultiCore definitions */
struct Request {
  void *buffer;
  int  sample;
  uint32_t frame_no;
  Request():buffer(0),sample(0),frame_no(0){}
};

struct Result {
  void *buffer;
  int  sample;
  uint32_t frame_no;
  int  channel;
};
~~~

これで、MainCoreの処理はできました。

## WiFiCore（WiFi通信用）inoについて

取得したデータをWiFi経由でPCに送る場合、別なSubCoreで処理をすることで、相互がそれぞれの処理を阻害しないようにできます。
そのため、今回は、WiFiの処理はSubCoreのinoに持ってきました。

連続データなので、UDPでデータを垂れ流しするようにします。
UDPでデータを受けるコードは、WiFiモジュールにあります。

[**GS2200-WiFi/examples/UDPClient/**
https://github.com/jittermaster/GS2200-WiFi/tree/master/examples/UDPClient](https://github.com/jittermaster/GS2200-WiFi/tree/master/examples/UDPClient)

ってことで、まずは、SubCore側でデータをUDPに送るサンプルを作ります。
送信処理に寄らずMainCoreからのデータは処理しないとえらーになってしまうので、送信処理を別なタスクにしておきます。

ということで、Setupは以下になります。

~~~ c
void setup()
{
  Serial.begin(115200);

  /* Initialize MP library */
  int ret = MP.begin();
  if (ret < 0) {
    errorLoop(2);
  }
  
  /* receive with non-blocking */
  MP.RecvTimeout(1);

  puts("WiFi Start");

  /* Initialize SPI access of GS2200 */
  Init_GS2200_SPI_type(iS110B_TypeA);
  /* Initialize AT Command Library Buffer */
  gsparams.mode = ATCMD_MODE_STATION;
  gsparams.psave = ATCMD_PSAVE_DEFAULT;
  if (gs2200.begin(gsparams)) {
    ConsoleLog("GS2200 Initilization Fails");
    while(1);
  }
  /* GS2200 Association to AP */
  if (gs2200.activate_station(AP_SSID, PASSPHRASE)) {
    ConsoleLog("Association Fails");
    while(1);
  }

  ConsoleLog("Start UDP Client");
  // Create UDP Client
  while(1){
    server_cid = gs2200.connectUDP(UDPSRVR_IP, UDPSRVR_PORT, LocalPort);
    ConsolePrintf("server_cid: %d \r\n", server_cid);
    if (server_cid != ATCMD_INVALID_CID) {
      puts("OK!");
      break;
    }
  }

  ConsoleLog("Start to send UDP Data");
  // Prepare for the next chunck of incoming data
  WiFi_InitESCBuffer();
  ConsolePrintf("\r\n");

  int connect = task_create("Connect", 70, 0x2000, connect_task, (FAR char* const*) 0);
}
~~~

MPライブラリとWiFiを初期化して、送信処理部分を別なタスクにするために、タスク生成を行います。

で、loopの中では、

~~~ c
void loop()
{
  int      ret;
  int8_t   rcvid;
  Request  *request;
  
  /* Receive PCM captured buffer from MainCore */
  ret = MP.Recv(&rcvid, &request);
  if (ret >= 0) {
    write_buffer(request->buffer,request->sample,request->frame_no);
  }

}

int write_buffer(uint16_t* data,int sample,uint32_t frame_no)
{
  for(int i=0;i<FRAME_NUMBER;i++){
    if(frame_buffer[i].wenable){
      frame_buffer[i].wenable = false;
      frame_buffer[i].sample = ((DATA_SIZE > sample) ? sample : DATA_SIZE);
      memcpy(&frame_buffer[i].buffer[HEADER_SIZE],data,sample*sizeof(uint16_t));
      frame_buffer[i].frame_no = frame_no;
      frame_buffer[i].renable = true;
      return 0;
    }
  }
  return 1;
}
~~~

というように、Mainのコアから受け取ったデータを自身のバッファにコピーだけします。

今回は、あまり時間がなかったので、ちゃんとしたFIFOではなく、エリアにフラグを付けて管理しただけのものになります。ごめんなさい。(^^;)。フレーム番号入れてるので狂ったらわかるので…。

タスク間でのデータのやり取りは、以下の構造体を利用しています。

~~~ c
truct FrameInfo{
  uint16_t buffer[HEADER_SIZE+DATA_SIZE+1];
  int32_t sample;
  uint32_t frame_no;
  bool renable;
  bool wenable;
  FrameInfo():renable(false),wenable(true){}
};

FrameInfo frame_buffer[FRAME_NUMBER];
~~~

このバッファに書き込んだら、wenableを落とし、renableを上げる。
このデータを読み込んで送信できたら、renableを落とし、renableを上げる感じです。

で、このデータをポーリングでwatchするタスクは、以下のようにしています。

~~~ c
int connect_task(int argc, FAR char *argv[])
{
  while(1){
    for(int i=0;i<FRAME_NUMBER;i++){
      if(frame_buffer[i].renable){
        if(frame_buffer[i].sample > 700) frame_buffer[i].sample = 700;
        send_data(frame_buffer[i]);
      }
    }
  }
}

#define SPI_MAX_SIZE   700
#define TXBUFFER_SIZE  SPI_MAX_SIZE

bool send_data(FrameInfo& frame)
{
  frame.renable = false;

  memcpy(frame.buffer,"SPRS",4);
  memcpy(&frame.buffer[2],&frame.sample,4);
  memcpy(&frame.buffer[4],&frame.frame_no,4);

  frame.buffer[HEADER_SIZE+frame.sample] = '\0';
  frame.sample += HEADER_SIZE+1;
  uint8_t* ptr = (uint8_t*)frame.buffer;

  while(frame.sample>0){
    int write_sample = ((frame.sample > TXBUFFER_SIZE) ? TXBUFFER_SIZE : frame.sample);
    printf("write_sample = %d\n",write_sample);
    bool ret = gs2200.write(server_cid, (const uint8_t*)ptr, write_sample*sizeof(uint16_t));
    if(!ret) return ret;
    ptr += write_sample*sizeof(uint16_t);
    frame.sample -= write_sample;
  }
  frame.wenable = true;
  return true;
}
~~~
これで、データがあれば、UDPで送信するようにしています。
ただ、UDPのデータなので、データの頭に、sync wordを入れて、サイズを埋め込んでおいています。

※終端コードも入れてもよいかなと思ってたりしますが、今回はそこまでやっていないです。


ということで、通信用のSubCoreのコードはできました！

# Processing側のpdeについて

Spresense側でデータを送ることができたので、PCのProcessing側で、UDP経由でデータを受け取り、可視化するアプリを作ります。今回は、今後、AIの開発をPCですることを踏まえ、波形を録音もできるようにします。

ProcessingのデータをUDPで受けるサンプルは、このあたりを参考にしました。

[**【Processing】UDPの送受信**
https://qiita.com/HanBei/items/b716ed113b83856f5231](https://qiita.com/HanBei/items/b716ed113b83856f5231)

今回は、受信側だけなので、受信のコードを参考に。

~~~ java
import hypermedia.net.*;
import controlP5.*;

import java.util.*;
import java.io.*;

UDP udp;
ControlP5 cp5;

// Please change the serial setting for user environment
final String IP = "192.168.2.139";
final int PORT = 10002;

String MODE_TYPE = "draw";
//String MODE_TYPE = "file";

final String  SAVE_FILE_NAME = "data/pcm.raw";
int           SAVE_DATA_SIZE = 100;

static int frame_sample = 1024;
static int max_data_number = frame_sample;

String msg = "test_messege";
OutputStream output;
~~~

presenseに割り当てられたIPとポートを指定して、data/pcm.rawに波形データを書きます。
書き込み用のフレームのサンプル数を指定して、ダミー送信用の文字列なども定義します。

で、setupで、

~~~ java
void setup()
{
  size(800, 400);
  background(255);

  udp = new UDP( this, 10001 );  
  udp.listen( true );

  if(MODE_TYPE.equals("file")){
      output = createOutput(SAVE_FILE_NAME);
  }

  UDP_Msg();
}

void UDP_Msg(){
  udp.send(msg,IP,PORT);
}
~~~

枠のサイズや色を定義し、自身のポートを定義して、書き込む場合はファイルをopenします。
その後、自分はまだ理由が分かっていないのですが、一度だけ、UDPのコマンドを出すと、recieveができるようになります。これでsetupは終わり。

で、receive の中身は、

~~~ java
byte [] recieve_data;
boolean recieve_data_ready = false;
int recieve_number = 0;
int rest_number = 0;
int frame_no = 0;

void receive( byte[] data, String ip, int port ) {

  if(rest_number > 0){
    byte [] tmp = concat(recieve_data,data);
    recieve_data = tmp;
    rest_number -= (data.length);
  }else if(!find_sync(data)){
    recover();
    return;
  }

  if(rest_number <= 0){
    recieve_data_ready = true;

    int now = millis();
    println( "receive: \""+data+"\" from "+ip+" on port "+port , "time=", now - base_time, "[ms]");
    base_time = now;
  }

  if(MODE_TYPE.equals("file")){
      save_data(recieve_data,recieve_data.length);
  }
}
~~~

こんな感じで、rest_number で、1回の受信で完了しなかった場合に、連続取得できるようにしています。が、それをしたら、ほぼ、リアルタイムでデータ取得ができなくなるので、不要だったかもしれません。

また、データの受信ができ、SYNCワードを見つけた場合に、recieve_data_ready を立てて、drawの指示をします。recieve_number にヘッダーから読み取ったデータサイズを入れます。
※ちょっとやっつけ気味でごめんなさい。

で、データの中に Syncワードを見つけるまでsearchします。

~~~ java
boolean find_sync(byte[] data)
{
  String sync_words = "0000";
  for(int i=0;i<data.length;){
    sync_words = sync_words.substring(1);
    sync_words = sync_words + (char)data[i];
    i++;
    if(sync_words.equals("SPRS")){
      recieve_number = data[i+3] & 0xff;
      recieve_number <<= 8;
      recieve_number += data[i+2] & 0xff;
      recieve_number <<= 8;
      recieve_number += data[i+1] & 0xff;
      recieve_number <<= 8;
      recieve_number += (data[i] & 0xff) + 1;
      recieve_number *= 2;
      i += 4;
      frame_no = data[i+3] & 0xff;
      frame_no <<= 8;
      frame_no += data[i+2] & 0xff;
      frame_no <<= 8;
      frame_no += data[i+1] & 0xff;
      frame_no <<= 8;
      frame_no += data[i] & 0xff;
      i += 4;
      println("frame_no = ",frame_no);
      recieve_data = Arrays.copyOfRange(data,(i),data.length);
      rest_number = recieve_number - (data.length) + i;
      if(rest_number <= 0) recieve_data_ready = true;
      return true;
    }
  }
  return false;
}
~~~

見つけたら、ヘッダからサイズとフレーム番号を読み出し、ヘッダ部分を切り落として、
描画OKのフラグを立てます。

ファイルを記録する場合は、save_dataでsaveします。

~~~ java
void save_data(byte[]data, int size)
{
  println("draw size = "+(size-4));
  
  for (int i=0; i < size-2; i=i+2) {
    try {
      if(SAVE_DATA_SIZE<0){
        output.flush();
        output.close();
        exit();
      }
      output.write(byte(data[i+1] & 0xff));
      output.write(byte(data[i] & 0xff));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  SAVE_DATA_SIZE--;
}
~~~

SAVE_DATA_SIZE だけ書いたら、書き込みとめて終了しちゃいます。

最後の描画の部分は、

~~~ java
void draw()
{
  if(!recieve_data_ready) return;

  if(MODE_TYPE.equals("draw")){
     draw_graph(recieve_data,recieve_data.length);
     recieve_data_ready = false;
  }
}

void draw_graph(byte[]data, int size)
{
   background(255);

   for (int i=4; i < size-4; i=i+2) {

    int data_i_s = int(data[i+1] << 8 | (data[i] & 0xff));
    int data_i_e = int(data[i+3] << 8 | (data[i+2] & 0xff));
    int stx = (int)map(i, 0,size, 0, width);
    int sty = (int)map(data_i_s, -32768, 32767, height, 0);
    int etx = (int)map(i, 0, size, 0, width);
    int ety = (int)map(data_i_e, -32768, 32767, height, 0);
    line(stx, sty, etx, ety);

   } 
}
~~~

という感じで、折れ線グラフを作成し、リアルタイムでの波形を出しています。

これを動かすと、こんな感じ。

https://twitter.com/spresense/status/1697337298709598451

これで、音声データを取得、保存することで、音声診断のAIの検討する準備ができました！

次に、生データではなくFFTによる周波数特性データを送るように変更する記事を書きます！