网站链接: element-ui dtcms
当前位置: 首页 > 技术博文  > 技术博文

ESP32+INMP441+DHT11+OLED+网页+Arduino——“智能”语音天气站(2):INMP441录音生成wav文件

2021/6/25 11:25:27 人评论

参考视频: https://www.youtube.com/watch?vqmruNKeIN-o 参考代码:学会了代码复用 https://github.com/0015/ThatProject/blob/master/ESP32_MICROPHONE/ESP32_INMP441_RECORDING/ESP32_INMP441_RECORDING.ino 知识 什么是wav文件 可以在维基百科找到…

参考视频:

https://www.youtube.com/watch?v=qmruNKeIN-o

参考代码:学会了代码复用

https://github.com/0015/ThatProject/blob/master/ESP32_MICROPHONE/ESP32_INMP441_RECORDING/ESP32_INMP441_RECORDING.ino

知识

什么是wav文件

可以在维基百科找到wav文件的历史渊源。这个网站有一个详尽的wav格式说明:

http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html

这里主要是想说wav文件重要的就是生成一个wavhead来标识它是一个wave文件,wave文件的data chunk 中的data部分还是PCM编码格式的数据,直接从I2S读进去就可以,不需要压缩。

wav head

wav文件的头一般由4个chunk组成,上面引用的网站里写的很清楚了。生成wav head的代码如下:

// 生成wav header,32bit 位深
void wavHeader(byte* header, int wavSize){ // 数字小端格式,字符大端格式
  header[0] = 'R';
  header[1] = 'I';
  header[2] = 'F';
  header[3] = 'F';
  unsigned int fileSize = wavSize + headerSize - 8;
  header[4] = (byte)(fileSize & 0xFF); // file size, 4byte integer
  header[5] = (byte)((fileSize >> 8) & 0xFF);
  header[6] = (byte)((fileSize >> 16) & 0xFF);
  header[7] = (byte)((fileSize >> 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; // length of format data = 16, 4byte integer
  header[17] = 0x00;
  header[18] = 0x00;
  header[19] = 0x00;
  header[20] = 0x01;  // format type:1(PCM), 2byte integer 
  header[21] = 0x00;
  header[22] = 0x01; // channel number:1, 2byte integer
  header[23] = 0x00;
  header[24] = 0x80; // sample rate:16000=0x00003E80, 4byte integer
  header[25] = 0x3E;
  header[26] = 0x00;
  header[27] = 0x00;
  header[28] = 0x00; // SampleRate*BitPerSample*ChannelNum/8=16000*32*1/8=64000=0x0000FA00, 4byte integer
  header[29] = 0xFA;
  header[30] = 0x00;
  header[31] = 0x00;
  header[32] = 0x04; // BitPerSample*ChannelNum/8 = 4, 2byte integer 
  header[33] = 0x00;
  header[34] = 0x20; // BitPerSample:32 = 0x0020, 2byte integer 
  header[35] = 0x00;
  header[36] = 'd';
  header[37] = 'a';
  header[38] = 't';
  header[39] = 'a';
  header[40] = (byte)(wavSize & 0xFF);
  header[41] = (byte)((wavSize >> 8) & 0xFF);
  header[42] = (byte)((wavSize >> 16) & 0xFF);
  header[43] = (byte)((wavSize >> 24) & 0xFF);
  
}

PCM编码

I2S工作

I²S 模式下,BCK 为串行时钟;WS 为通道选择信号,用于表示左右声道的切换;SD 为串行数据信号,传输音频数据。WS 信号和 SD 信号在 BCK 的下降沿发生变化,并在 BCK 的上升沿采样 SD 信号。

I2S接口标准

I2S支持三种接口标准。(ESP32 技术参考手册)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
查询INMP441的资料,设置为左单通道输入时,其采用的接口标准为Philips 标准,即WS领先SD一个时钟周期变化:
在这里插入图片描述
在初始化I2S的时候,接口标准采用I2S_COMM_FORMAT_I2S。不过其他的接口标准也会实验一下,因为不知道什么原因,麦克风录音发生了截断,考虑从几个方面去解决这个问题:

  1. 接口标准
  2. scale方法
  3. 在SD管脚处接入下拉电阻
  4. SPIFFS的读写速度原因
    方法三是基于INMP441的资料,其中建议到:SD管脚应该有一个下拉电阻,在总线上的所有麦克风都有三态输出时discharge。

数模转换

i2s_read()函数,向flash种划分的一个buff当中读入byte数组。但是注意一个样本值是由32个bit记录的,也就是说byte数组当中每四个byte就记录着一个sample。在将buff中的数值写入wav文件时,要把数字信号转换成模拟信号才可以被电脑上的播放器播放,所以要通过一个scale函数:

void i2s_adc_data_scale(uint8_t * d_buff, uint8_t* s_buff, uint32_t len)
{
    uint32_t j = 0;
    uint32_t dac_value = 0;
    // 一个采样点是4byte,每4个byte
    for (int i = 0; i < len; i += 4) {
        dac_value = ((((uint16_t)(s_buff[i + 3] & 0xf) << 8) | ((s_buff[i + 2]))));
        d_buff[j++] = 0;
        d_buff[j++] = 0;
        d_buff[j++] = 0;
        d_buff[j++] = dac_value * 128 / 2048; // divided by 4096 if you want it to be lower
    }
}

把buff中每四个byte提取出来,将最后两个byte拼接起来组成一个无符号16位整型,再转换成sample对应的电压值。转换的原理我也不是很清楚,希望有人可以指导一下。
看这一行代码:

dac_value = ((((uint16_t)(s_buff[i + 3] & 0xf) << 8) | ((s_buff[i + 2]))));

把byte数组转换成16位的unsigned int类型,因为是32bit所以只取了最后两个byte(为什么?)

d_buff[j++] = dac_value * 128 / 2048;

把它转换成电压值。
最后的乘除因为运算的都是2的指数,可以通过移位来加快速度:
乘以2n ,左移n,除以 2n, 右移n
变成:

d_buff[j++] = (dac_value << 8) >> 12 ;

SPIFFS

这是esp32管理文件的系统。要注意esp32的文件空间不大,如果上传文件太多的话会把空间占满,就无法再上传文件,所以上传文件之前要把多于的文件从esp32的文件空间中删去。
本次代码时直接在运行过程中生成文件,但是如果想把已存在的文件上传到esp32的文件系统,首先要在工程文件夹中创建data文件夹,把文件放进去,然后使用arduino工具esp32 sketch data upload上传文件,就可以用SPIFFS打开了。
listSPIFFS()是SPIFFS示例中的代码。
工具esp32 sketch data upload:

https://github.com/me-no-dev/arduino-esp32fs-plugin

void SPIFFSInit(){
  if(!SPIFFS.begin(true)){
    Serial.println("SPIFFS initialisation failed!");
    while(1) yield();
  }

  SPIFFS.remove(filename);// 移除掉已存在的file
  file = SPIFFS.open(filename, FILE_WRITE);
  if(!file){
    Serial.println("File is not available!");
  }

  byte header[headerSize];
  wavHeader(header, FLASH_RECORD_SIZE); // 生成wave header

  file.write(header, headerSize); // 写入header
  listSPIFFS();
}

代码

/*
 * 使用i2s录音2秒,并生成recording.wav文件,上传到File Browser当中
*/
#include <driver/i2s.h>
#include <SPIFFS.h>

#define I2S_WS 15
#define I2S_SD 13
#define I2S_SCK 2
#define I2S_PORT I2S_NUM_0
#define I2S_SAMPLE_RATE   (16000)
#define I2S_SAMPLE_BITS   (32)
#define I2S_READ_LEN      (32 * 1024) 
#define RECORD_TIME       (2) //Seconds
#define I2S_CHANNEL_NUM   (1)
#define FLASH_RECORD_SIZE (I2S_CHANNEL_NUM * I2S_SAMPLE_RATE * I2S_SAMPLE_BITS / 8 * RECORD_TIME)

File file;
const char filename[] = "/recording.wav";
const int headerSize = 44; // wave header size

void setup() {
  Serial.begin(115200);
  SPIFFSInit();
  i2sInit();
  xTaskCreate(i2s_adc, "i2s_adc", 1024 * 2, NULL, 1, NULL);
}

void loop() {}

void SPIFFSInit(){
  if(!SPIFFS.begin(true)){
    Serial.println("SPIFFS initialisation failed!");
    while(1) yield();
  }

  SPIFFS.remove(filename);// 移除掉已存在的file
  file = SPIFFS.open(filename, FILE_WRITE);
  if(!file){
    Serial.println("File is not available!");
  }

  byte header[headerSize];
  wavHeader(header, FLASH_RECORD_SIZE); // 生成wave header

  file.write(header, headerSize); // 写入header
  listSPIFFS();
}

// 初始化i2s,原理和Plotter中的代码相同
void i2sInit(){
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = I2S_SAMPLE_RATE,
    .bits_per_sample = i2s_bits_per_sample_t(I2S_SAMPLE_BITS),
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_PCM), // PCM standard 
    .intr_alloc_flags = 0,
    .dma_buf_count = 32,  // 改成32count之后读取内存报错消失
    .dma_buf_len = 1024,  // 1024 samples per buffer
    .use_apll = 1        // use APLL-CLK,frequency 16MHZ-128MHZ,it's for audio
  };

  i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);

  const i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_SCK,
    .ws_io_num = I2S_WS,
    .data_out_num = -1,
    .data_in_num = I2S_SD
  };

  i2s_set_pin(I2S_PORT, &pin_config);
}


// digital to analog:麦克风输入为digital code(binary), 需要将编码转换成相应的电压值才能播放
void i2s_adc_data_scale(uint8_t * d_buff, uint8_t* s_buff, uint32_t len)
{
    uint32_t j = 0;
    uint32_t dac_value = 0;
    // 一个采样点是4byte,每4个byte
    for (int i = 0; i < len; i += 4) {
        dac_value = ((((uint16_t)(s_buff[i + 3] & 0xf) << 8) | ((s_buff[i + 2]))));
        d_buff[j++] = 0;
        d_buff[j++] = 0;
        d_buff[j++] = 0;
        d_buff[j++] = dac_value * 128 / 2048;
    }
}

// 录音任务
void i2s_adc(void *arg)
{
    
    int i2s_read_len = I2S_READ_LEN;
    // 已写入flash的大小
    int flash_wr_size = 0;
    // in i2s_read(),Number of bytes read, if timeout, bytes read will be less than the size passed in
    size_t bytes_read;
    // 每一次读取一个i2s_read_buff,大小:32 * 1024 bytes,DMA Buffer大小应该是其倍数,不然会报错:读取内存错误
    char* i2s_read_buff = (char*) calloc(i2s_read_len, sizeof(char));
    // 经过scale之后每次读取写入flash的buff
    uint8_t* flash_write_buff = (uint8_t*) calloc(i2s_read_len, sizeof(char));

    // 已经开始写入了,试试删掉,因为INMP441需要准备时间
    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);
    
    Serial.println(" *** Recording Start *** ");
    while (flash_wr_size < FLASH_RECORD_SIZE) {
        //read data from I2S bus.
        i2s_read(I2S_PORT, (void*) i2s_read_buff, i2s_read_len, &bytes_read, portMAX_DELAY);
        // 展示一下都录了什么
        example_disp_buf((uint8_t*) i2s_read_buff, 32);
        //save original data from I2S into flash.
        // 先转换成电压值
        i2s_adc_data_scale(flash_write_buff, (uint8_t*)i2s_read_buff, i2s_read_len);
        file.write((const byte*)  flash_write_buff, i2s_read_len);
        flash_wr_size += i2s_read_len;
        ets_printf("Sound recording %u%%\n", flash_wr_size * 100 / FLASH_RECORD_SIZE);
        ets_printf("Never Used Stack Size: %u\n", uxTaskGetStackHighWaterMark(NULL));
    }
    file.close();
    // 清空buff
    free(i2s_read_buff);
    i2s_read_buff = NULL;
    free(flash_write_buff);
    flash_write_buff = NULL;
    
    listSPIFFS();
    vTaskDelete(NULL);
}

// 展示buff内容的函数
void example_disp_buf(uint8_t* buf, int length)
{
    printf("======\n");
    for (int i = 0; i < length; i++) {
        printf("%02x ", buf[i]);
        if ((i + 1) % 8 == 0) {
            printf("\n");
        }
    }
    printf("======\n");
}

// 生成wav header,32bit 位深
void wavHeader(byte* header, int wavSize){ // 数字小端格式,字符大端格式
  header[0] = 'R';
  header[1] = 'I';
  header[2] = 'F';
  header[3] = 'F';
  unsigned int fileSize = wavSize + headerSize - 8;
  header[4] = (byte)(fileSize & 0xFF); // file size, 4byte integer
  header[5] = (byte)((fileSize >> 8) & 0xFF);
  header[6] = (byte)((fileSize >> 16) & 0xFF);
  header[7] = (byte)((fileSize >> 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; // length of format data = 16, 4byte integer
  header[17] = 0x00;
  header[18] = 0x00;
  header[19] = 0x00;
  header[20] = 0x01;  // format type:1(PCM), 2byte integer 
  header[21] = 0x00;
  header[22] = 0x01; // channel number:1, 2byte integer
  header[23] = 0x00;
  header[24] = 0x80; // sample rate:16000=0x00003E80, 4byte integer
  header[25] = 0x3E;
  header[26] = 0x00;
  header[27] = 0x00;
  header[28] = 0x00; // SampleRate*BitPerSample*ChannelNum/8=16000*32*1/8=64000=0x0000FA00, 4byte integer
  header[29] = 0xFA;
  header[30] = 0x00;
  header[31] = 0x00;
  header[32] = 0x04; // BitPerSample*ChannelNum/8 = 4, 2byte integer 
  header[33] = 0x00;
  header[34] = 0x20; // BitPerSample:32 = 0x0020, 2byte integer 
  header[35] = 0x00;
  header[36] = 'd';
  header[37] = 'a';
  header[38] = 't';
  header[39] = 'a';
  header[40] = (byte)(wavSize & 0xFF);
  header[41] = (byte)((wavSize >> 8) & 0xFF);
  header[42] = (byte)((wavSize >> 16) & 0xFF);
  header[43] = (byte)((wavSize >> 24) & 0xFF);
  
}


void listSPIFFS(void) {
  Serial.println(F("\r\nListing SPIFFS files:"));
  static const char line[] PROGMEM =  "=================================================";

  Serial.println(FPSTR(line));
  Serial.println(F("  File name                              Size"));
  Serial.println(FPSTR(line));

  fs::File root = SPIFFS.open("/");
  if (!root) {
    Serial.println(F("Failed to open directory"));
    return;
  }
  if (!root.isDirectory()) {
    Serial.println(F("Not a directory"));
    return;
  }

  fs::File file = root.openNextFile();
  while (file) {

    if (file.isDirectory()) {
      Serial.print("DIR : ");
      String fileName = file.name();
      Serial.print(fileName);
    } else {
      String fileName = file.name();
      Serial.print("  " + fileName);
      // File path can be 31 characters maximum in SPIFFS
      int spaces = 33 - fileName.length(); // Tabulate nicely
      if (spaces < 1) spaces = 1;
      while (spaces--) Serial.print(" ");
      String fileSize = (String) file.size();
      spaces = 10 - fileSize.length(); // Tabulate nicely
      if (spaces < 1) spaces = 1;
      while (spaces--) Serial.print(" ");
      Serial.println(fileSize + " bytes");
    }

    file = root.openNextFile();
  }

  Serial.println(FPSTR(line));
  Serial.println();
  delay(1000);
}

出现的问题和解决

内存出错

参考代码的麦克风可以进行16bi位深的录音,但是因为我们的麦克风限制,我改成32bit之后出现了如下报错,查询官方文档(ESP-IDF 编程指南),了解错误原因:

Guru Meditation Error: Core panic'ed StoreProhibited

当应用程序尝试读取或写入无效的内存位置时,会发生此类CPU 异常。此类无效内存地址可以在寄存器转储的EXCVADDR 中找到。如果该地址为零,通常意味着应用程序正尝试解引用一个NULL 指针。如果该地址接近于零,则通常意味着应用程序尝试访问某个结构体的成员,但是该结构体的指针为NULL。
如果该地址是其它非法值(不在0x3fxxxxxx - 0x6xxxxxxx 的范围内),则可能意味着用于访问数据的指针未初始化或者已经损坏。

使用arduino工具Esp Exception Decoder解析得到具体出错的代码文件、函数和行号。这里没有截图了。
下载地址:

https://github.com/me-no-dev/EspExceptionDecoder

虽然找到了出错行,也并没有让我找到解决办法,既然是内存读取或写入出错,那么应该是I2S在执行DMA读写的时候因为内存不相匹配导致指针指向了无效的位置,所以尝试将DMA的count改成了32之后,报错消失。

相关资讯

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?