参考视频:
Recording using INMP441

参考代码:学会了代码复用
Recording using INMP441

知识

什么是wav文件

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

这里主要是想说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 integerheader[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 integerheader[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 integerheader[23] = 0x00;header[24] = 0x80; // sample rate:16000=0x00003E80, 4byte integerheader[25] = 0x3E;header[26] = 0x00;header[27] = 0x00;header[28] = 0x00; // SampleRate*BitPerSample*ChannelNum/8=16000*32*1/8=64000=0x0000FA00, 4byte integerheader[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个bytefor (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:
esp32 sketch data upload

void SPIFFSInit(){if(!SPIFFS.begin(true)){Serial.println("SPIFFS initialisation failed!");while(1) yield();}SPIFFS.remove(filename);// 移除掉已存在的filefile = SPIFFS.open(filename, FILE_WRITE);if(!file){Serial.println("File is not available!");}byte header[headerSize];wavHeader(header, FLASH_RECORD_SIZE); // 生成wave headerfile.write(header, headerSize); // 写入headerlistSPIFFS();
}

代码

/** 使用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 sizevoid 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);// 移除掉已存在的filefile = SPIFFS.open(filename, FILE_WRITE);if(!file){Serial.println("File is not available!");}byte header[headerSize];wavHeader(header, FLASH_RECORD_SIZE); // 生成wave headerfile.write(header, headerSize); // 写入headerlistSPIFFS();
}// 初始化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个bytefor (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 insize_t bytes_read;// 每一次读取一个i2s_read_buff,大小:32 * 1024 bytes,DMA Buffer大小应该是其倍数,不然会报错:读取内存错误char* i2s_read_buff = (char*) calloc(i2s_read_len, sizeof(char));// 经过scale之后每次读取写入flash的buffuint8_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();// 清空bufffree(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 integerheader[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 integerheader[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 integerheader[23] = 0x00;header[24] = 0x80; // sample rate:16000=0x00003E80, 4byte integerheader[25] = 0x3E;header[26] = 0x00;header[27] = 0x00;header[28] = 0x00; // SampleRate*BitPerSample*ChannelNum/8=16000*32*1/8=64000=0x0000FA00, 4byte integerheader[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 SPIFFSint spaces = 33 - fileName.length(); // Tabulate nicelyif (spaces < 1) spaces = 1;while (spaces--) Serial.print(" ");String fileSize = (String) file.size();spaces = 10 - fileSize.length(); // Tabulate nicelyif (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解析得到具体出错的代码文件、函数和行号。这里没有截图了。
下载地址:

Esp Exception Decoder

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

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

  1. python语音合成并播放_使用Python实现文字转语音并生成wav文件的例子

    目前手边的一些工作,需要实现声音播放功能,而且仅支持wav声音格式. 现在,一些网站上支持文字转语音功能,但是生成的都是MP3文件,这样还需要额外的软件来转成wav文件,十分麻烦. 后来,研究Pyth ...

  2. biu~ 你的智能语音客服已免费一键生成!

    在智能化发展的大趋势下,为了提供更高效的服务,越来越多的企业开始将智能语音客服融合到自己的业务或产品中. 传统的人工客服,培训周期长.专业能力参差不齐.上岗时间也会有所限制,然而一个合格的客服不仅需要 ...

  3. C#文字转语音,实时播放以及mp3,wav文件保存

    源码下载地址:https://download.csdn.net/download/horseroll/10500847 无积分付费下载地址:https://download.csdn.net/dow ...

  4. QT+讯飞智能语音在线识别demo,录音识别

    目录 前言(feihua) 特性: 使用前提: 使用步骤: 软件获取方式 前言(feihua) 本程序使用讯飞的在线语音听写websocket API实现语音识别,在网络条件良好的前提下,识别速度是很 ...

  5. 贴近司机,感知生活:智能语音助手在滴滴车主端的设计与实践

    桔妹导读:基于网约车司机的职业特性,帮助与指引司机在各类复杂的场景下更安全.便捷地完成工作,并尽可能疏导与减轻他们因长时间处于封闭环境下的心理压力,一直是滴滴发力的一个方向.但现有的一些途径,如规则展 ...

  6. 【离线文本转语音文件】java spring boot jacob实现文字转语音文件,离线文本转化语音,中英文生成语音,文字朗读,中文生成声音,文字生成声音文件,文字转语音文件,文字变声音。

    1.实现效果如下: 输入文字(支持中英文),点击转换生成***.wav文件,点击下载到本地就可. 生成后的音频文件播放,时长1分8秒 2.实现代码: 这次采用jacob实现,相比百度AI需要联网,本项 ...

  7. ESP32+arduino智能浇水系统

    一.ESP32+arduino智能浇水系统 随着人类居住条件的改善及对生态生活环境的关注,花卉养殖得到社会和人类个体的重视.这些具有生命特征的植物需 要科学合理的人工照顾.本研究提出了利用ESP WR ...

  8. 使用ESP8266/ESP32 实现智能语音控制电脑开关机

    最近买了个台机放客厅里接电视玩游戏,另外还有跑程序计算的需求所以通过笔记本电脑使用RDP或Parsec来远程控制,当然瘾犯了也能云游戏.路由器在另一个房间所以是无线网卡链接,用的最好的AX210+10 ...

  9. 基于云平台的智能语音交互式灌溉系统

    ---------------------------------------------------------------------------------------------------- ...

最新文章

  1. #串口通信超时处理_实现4G无线通信透传的远程通信多组网5个PLC相互交换数据...
  2. 【caffe】基本数据结构blob
  3. OpenGL剪切平面和双面渲染
  4. AS3 CookBook学习整理(一)
  5. centos7安装redmine3,并升级redmine1.8到3
  6. Java从零开始学三十六(JAVA IO- 字符流)
  7. redistemplate给hash存储设置有效期_客户端较为常用的存储机制
  8. Kubernetes详解(九)——资源配置清单创建Pod实战
  9. linux信号量配合共享内存应用分析(详解)
  10. Keil 中 Error L6002U
  11. 圆柱螺旋压缩弹簧计算实例
  12. 讯飞在线语音TTS队列策略
  13. java8新特性,stream流多种写法
  14. 中年网络工程师如何转型?
  15. 微信小程序时间戳转换为日期
  16. qe和qc的区别在哪里_QC, QE,QA,QO的具体定义是什么,工厂里面个岗位具体职能又是?...
  17. 高效办公小妙招(三)——15个鲜为人知的的小众网站
  18. Dijkstra——最短路径路由算法java实现
  19. 目前最新最全的xp操作系统大全
  20. mysql自定义函数-随机生成人员姓名

热门文章

  1. 网络重置后无法上网,以太网和无线网全部丢失,网络适配器出现“56”错误码
  2. 2020西安邮电大学linux兴趣小组补录
  3. Bezier(贝塞尔)曲线小总结
  4. 学网页UI设计培训后能做什么呢
  5. 来闯关吗?​一个有趣的 Python 解谜网站
  6. QT 遍历一个文件夹下的所有图片。
  7. http 502 bad gate way
  8. docker部署的nginx HTTP ERROR 502怎么办
  9. 【021】ColorHexa –关于色彩的百科全书
  10. 记录每日待办事项的APP软件