4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpresenseAdvent Calendar 2023

Day 6

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

Last updated at Posted at 2023-12-05

初めに

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

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

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

用意したもの

SPRESENSEメインボード[CXD5602PWBMAIN1]
https://www.switch-science.com/catalog/3900/

SPRESENSE拡張ボード[CXD5602PWBEXT1]
https://www.switch-science.com/products/3901

SPRESENSE Wi-Fi Add-onボード iS110B
https://idy-design.com/product/is110b.html

B-stem 4CM01 SPRESENSE用4chマイク基板
https://www.switch-science.com/products/7996

全方向性マイク カー外部マイク 高感度 コンデンサーマイク GPS DVDラジオ用の車の外部マイク- 2.5mm直角 黒
https://www.amazon.co.jp/-/en/Omnidirectional-Microphone-External-Sensitivity-Condenser/dp/B08YYYJQCD

ブログ(元ネタ)

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

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

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

音響診断AIを作るためにProcessingを使って準備しよう!(Processing編)
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

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

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

#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ライブラリの初期化などを追加します。

/**
 *  @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 の中で処理しています。

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に送っています。

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間で共有して使います。

/* 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

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

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

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の中では、

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ではなく、エリアにフラグを付けて管理しただけのものになります。ごめんなさい。(^^;)。フレーム番号入れてるので狂ったらわかるので…。

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

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するタスクは、以下のようにしています。

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

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

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で、

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 の中身は、

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します。

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します。

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 だけ書いたら、書き込みとめて終了しちゃいます。

最後の描画の部分は、

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

   } 
}

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

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

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

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

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?