0
0
Qiita×Findy記事投稿キャンペーン 「自分のエンジニアとしてのキャリアを振り返ろう!」

I2Sデジタルマイクで録音、BTシリアルで受信してWAVファイルにしてSpeechRecognition

Posted at

先日、とあるイベントでESP32に接続された謎のI2S接続のデジタルマイクを制御することになった。
色々解説サイトやGithubを漁ったが、ちゃんとWAVファイル化して音声解析に食わせられるものがなかったので自作。

ESP32側はメモリがないため、DMA転送された音声データをほとんどキャッシュできない。
かなり力技であるがBTシリアルで送信してPC側で録音データを組み立てる案でやってみた。

SPH0645LM4HなどのI2Sデジタルマイクは結構癖がある使い方。
録音データは16kHz/32bitとのこと。本当はデバイスが2つないとステレオにならない模様なのでステレオ録音してLチャンネルの録音データのみ取り出した。
音量が小さいのをビットシフトで調整して0点オフセットかけて取りあえず聴ける。

PC側でWAVファイルとして聞く場合、16kHz/16bitのフォーマットでないとちゃんと聞けなかった。ここにドはまり。
32bitを16bitに削って16kHz/16bitモノラルとして、下記コードで実際録音してみたが、しゃべり内容は十分聞き取れるレベルであった。SpeechRecognitionでも解析OK!

ESP32で動作するコードです。ESP32ライブラリは1.0.6推奨。
WAVヘッダは手打ちで書いてますが、PC側アプリでつけた方が良いです。

pin config通りにデジタルマイクを配線し、ESP32起動したら待ち時間内にPythonアプリを起動してください。

i2s_mic.ino.cpp
#include "driver/i2s.h"

#include "BluetoothSerial.h"
BluetoothSerial SerialBT;

#define CONFIG_EXAMPLE_REC_TIME (5)
#define CONFIG_EXAMPLE_SAMPLE_RATE (16000)
#define NUM_CHANNELS        (1)
#define SAMPLE_SIZE         (I2S_BITS_PER_SAMPLE_32BIT * 1024)
#define BYTE_RATE           ((CONFIG_EXAMPLE_SAMPLE_RATE * (I2S_BITS_PER_SAMPLE_32BIT / 8)) * NUM_CHANNELS)

/////////////////////////////////////////////////////////////////////
// デジタルマイク用I2S
const i2s_port_t   I2S_PORT   = I2S_NUM_1;
const i2s_config_t i2s_config = {
   .mode                =i2s_mode_t(I2S_MODE_MASTER|I2S_MODE_RX),
   .sample_rate         =CONFIG_EXAMPLE_SAMPLE_RATE,  // サンプリングレート
   .bits_per_sample     =I2S_BITS_PER_SAMPLE_32BIT,   // 32bit
   .channel_format      =I2S_CHANNEL_FMT_RIGHT_LEFT, // ステレオ
   //.channel_format      =I2S_CHANNEL_FMT_ONLY_LEFT,   // only left
   .communication_format=I2S_COMM_FORMAT_PCM,         // PCM
   .intr_alloc_flags    =ESP_INTR_FLAG_LEVEL1,        // Interrupt level 1
   .dma_buf_count       =4,                          // number of buffers
   .dma_buf_len         =32                         //  samples per buffer
   };

const i2s_pin_config_t pin_config = {
   .bck_io_num   = 16, // BCLK
   .ws_io_num    = 17, // LRCL
   .data_out_num = -1, // マイクでは使わない
   .data_in_num  = 26  // DOUT
   };
const static int val_shift = 13;    // ボリューム調整
const static int val_offset= 14000; // 0点補正


// 16kHz/16bitのWAVとして再生できるように
void CreateWavHeader(byte* header, int waveDataSize){
  header[0] = 'R';
  header[1] = 'I';
  header[2] = 'F';
  header[3] = 'F';
  unsigned int fileSizeMinus8 = waveDataSize + 44 - 8;
  header[4] = (byte)(fileSizeMinus8 & 0xFF);
  header[5] = (byte)((fileSizeMinus8 >> 8) & 0xFF);
  header[6] = (byte)((fileSizeMinus8 >> 16) & 0xFF);
  header[7] = (byte)((fileSizeMinus8 >> 24) & 0xFF);
  header[8] = 'W';
  header[9] = 'A';
  header[10] = 'V';
  header[11] = 'E';
  header[12] = 'f';
  header[13] = 'm';
  header[14] = 't';
  header[15] = ' ';
  header[16] = 0x10;  // linear PCM
  header[17] = 0x00;
  header[18] = 0x00;
  header[19] = 0x00;
  header[20] = 0x01;  // linear PCM
  header[21] = 0x00;
  header[22] = 0x01;  // monoral
  header[23] = 0x00;
  header[24] = 0x80;  // sampling rate 16000(0x3E80)
  header[25] = 0x3E;
  header[26] = 0x00;
  header[27] = 0x00;
  header[28] = 0x00;  // Byte/sec = 16kHz x 16bit x monoral 16000*2*1 = 32000(7D00)
  header[29] = 0x7D;
  header[30] = 0x00;
  header[31] = 0x00;
  header[32] = 0x02;  // 16bit monoral
  header[33] = 0x00;
  header[34] = 0x10;  // 16bit(WAVは8か16)
  header[35] = 0x00;
  header[36] = 'd';
  header[37] = 'a';
  header[38] = 't';
  header[39] = 'a';
  header[40] = (byte)(waveDataSize & 0xFF);
  header[41] = (byte)((waveDataSize >> 8) & 0xFF);
  header[42] = (byte)((waveDataSize >> 16) & 0xFF);
  header[43] = (byte)((waveDataSize >> 24) & 0xFF);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Configuring I2S...");
  SerialBT.begin("ESP32test");

  // デジタルマイク
  esp_err_t _err;
  _err = i2s_driver_install(I2S_PORT,&i2s_config, 0, NULL);
  if (_err != ESP_OK) {
    Serial.printf("Failed installing driver: %d\n", _err);
    while (true);
  }
  _err = i2s_set_pin(I2S_PORT, &pin_config);
  if (_err != ESP_OK) {
    Serial.printf("Failed setting pin: %d\n", _err);
    while (true);
  }
  Serial.println("I2S driver installed.");

  // 起動したらPythonアプリが録音待ちになるまで待つ
  Serial.printf("Count down...");
  uint32_t delaytime = 15;
  for (int i=0; i<delaytime; i++){
    Serial.printf("%d,", delaytime-i);
    delay(1000); 
  }
  Serial.printf("Start REC!\n");


  // WAVヘッダを送信
  uint32_t rec_size = BYTE_RATE * CONFIG_EXAMPLE_REC_TIME;
  const char wav_header[44]={0};
  CreateWavHeader((byte*) wav_header, rec_size);
  SerialBT.write((const uint8_t *)&wav_header, sizeof(wav_header));

  record_wav(CONFIG_EXAMPLE_REC_TIME);

  Serial.printf("End REC!\n");
}

void record_wav(uint32_t rec_time)
{
  int i2s_read_len = SAMPLE_SIZE;
  size_t bytes_read;// ここに読み込んだ長さ
  char* i2s_read_buff = (char*) calloc(i2s_read_len, sizeof(char));

  // 空読み
  i2s_read(I2S_PORT, (void*) i2s_read_buff, i2s_read_len, &bytes_read, portMAX_DELAY);
  i2s_read(I2S_PORT, (void*) i2s_read_buff, i2s_read_len, &bytes_read, portMAX_DELAY);

  int data_wr_size = 0;
  uint32_t record_size = BYTE_RATE * rec_time;
  int32_t   _samples[2];	// I2SのDMAからは32bit x LRチャンネル分得る
  while (data_wr_size < record_size)
  {
    int16_t _vL[128]; // ★WAVは16bit
    for(int i=0; i < 128; i++){
      i2s_read(I2S_PORT, (void*) _samples, 8, &bytes_read, portMAX_DELAY);	// DMA転送は4byte(32bit)で得る
      _vL[i] = (_samples[1]>>val_shift)+val_offset; // 音量と左右位置調整 (★ここで4byte→2byteに落とす)
    }
    SerialBT.write((const uint8_t*)_vL, 256);
    data_wr_size += 256;
  }
  Serial.println(data_wr_size);
}

void loop() {
}
data_recv.py
import serial
import speech_recognition as sr

# BTシリアルなので普通のシリアルポートとして見えます
com = serial.Serial('COM7', 115200)

f = open('test1.wav', 'wb')
s = ""
read_byte = 256
end_size = 320000
datanum = 0
while True:
    data = com.read(read_byte)
    f.write(data)
    print(data)
    datanum = datanum + read_byte
    if datanum >= end_size:
        print("end recv.")
        break

f.close()

com.close()

# WAVファイルから音声解析
r = sr.Recognizer()
with sr.AudioFile("test1.wav") as source:
    audio_d = r.listen(source)

    print(r.recognize_google(audio_d, language='ja-JP'))

0
0
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
0
0