初めに
Spresenseは、音響診断に向いているとよく言われていますが、現時点で音響診断用のDNN のネットワークは画像処理ほど、豊富でなく自分自身でトライを繰り返す必要があります。
そこで、いきなり実機でのトライをするより、PC上で可視化しながら DNNネットワークを構築したいと思い、生波形の可視化とデータ記録をProcessingで行なってみました!
以下の記事(Processing+Spresense)を参考にしています。
用意したもの
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では、主に音声の取得を行います。
音声の生データを取得するサンプルは、ここにあるので、
このコードを改変するようにします。
ってことで、まずは変数定義ですが、
#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モジュールにあります。
ってことで、まずは、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による周波数特性データを送るように変更する記事を書きます!