先日、とあるイベントで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アプリを起動してください。
#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() {
}
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'))