esp32笔记[5]

您所在的位置:网站首页 esp32音频流传到服务器 esp32笔记[5]

esp32笔记[5]

2024-07-11 19:29| 来源: 网络整理| 查看: 265

摘要

基于I2S协议实现音频播放,制作一个可以通过串口点播音频的语音播放模块。

硬件平台 ESP32-S3开发板 //IO口 #define SPEAKER_WS 7 #define SPEAKER_SCK 16 #define SPEAKER_DATA 6 #define USART0_RX 44 #define USART0_TX 43 NS4168简介

NS4168为D类功放,使用I2S协议。 NS4168是一款支持I2S数字音频信号输入,输出具有防失真功能,2.5W单声道D类音频功率放大器。NS4168特别适用于对功耗敏感而产生干扰的环境。比如蓝牙音响,WiFi音响,平板电脑等。无需使用输入耦合电容,通过CTRL管脚检测一线脉冲选择内部输入高通滤波器的转折点以匹配不同喇叭。layout时无需精心考虑音频功放的布局以及走线,外围更简洁,调试更方便。 NS4168其独特的防失真功能可以有效防止输入信号过载、电池电压下降导致的输出信号失真,同时可以有效保护在大功率输出时扬声器不被损坏。 NS4168采用高效率、低噪声调制方案,无需外部LC输出滤波器。闭环多级调制器设计保留了纯数字放大器高效率的优势,同时又具有极佳的PSRR和音频性能。与其他D类架构相比,采用扩频脉冲密度调制可提供更低的电磁辐射。NS4168在5V的工作电压时,能够向4Ω负载提供2.5W的输出功率。 NS4168为单声道音频功放。左右声道选择通过CTRL管脚电平设置。立体声产品可选用两个芯片,非常灵活。 NS4168内置过流保护、过热保护及欠压保护功能,有效地保护芯片在异常工作状况下不被损坏。提供eSOP8封装,额定的工作温度范围为-40°C至85°C。

D类功放简介

[https://zhuanlan.zhihu.com/p/466496321]

D类功放的基本组成: 包括调制器,开关放大器,低通滤波器,图上的换能器一般为将电信号转换为音频信号的喇叭。

工作原理: 假设低频的正弦波为信号源a,经过比较器和三角波进行进行比较,得到占空比不同的PWM调制(此时输出信号的脉宽与输入信号的幅值成正比),再经过由PMOS,NMOS组成的推挽输出进行放大和反向(幅度放大到与VDD相同),放大后的信号经过低通滤波器LC后(假设滤波器的截止频率比输出级的开关频率至少低一个数量级)它的输出是方波的平均值,还原成模拟信号正弦波。

关于低通滤波器 LC低通滤波器,可以将高频能量滤除,防止阻性负载上耗散高频开关能量(但是目前D类功放大部分都是filterless)。假设滤波后的输出电压(VO_AVG)和电流(IAVG)在单个开关周期内保持恒定。这种假设较为准确,因为fSW比音频输入信号的最高频率要高得多。因此,占空比与滤波后的输出电压之间的关系,可通过对电感电压和电流进行简单的时间域分析得到。

实现音频播放的相关协议 WAV文件编码格式

WAV即WAVE,是经典的Windows音频数据封装格式,由Microsoft开发。数据本身格式为PCM,也可以支持一些编码格式的数据,比如最近流行的AAC编码。如果是PCM,则为无损格式,文件会比较大,并且大小相对固定,可以使用以下公式计算文件大小。

FileSize = HeadSize + TimeInSecond * SampleRate * Channels * BitsPerSample / 8

其中HeadSize为WAV文件头部长度;SampleRate,即采样率,可选8000、16000、32000、44100或48000;Channels表示声道数量,通常为1或2;BitsPerSample代表单个Sample的位深,可选8、16以及32,其中32位时可以是float类型。   WAV是一种极其简单的文件格式,如果对其结构足够熟悉,完全可以自己通过代码写入WAV文件,从而免去引入一些复杂中间库。特别是在对音频进行调试的时候,能提高效率,降低复杂度。 WAV格式遵循RIFF规范,所有WAV都有一个文件头,记录着音频流的采样和编码信息。数据块的记录方式是小尾端(little-endian)。 只有ID为"RIFF"或者"LIST"的块允许拥有子块(SubChunk)。RIFF文件的第一个块的ID必须是"RIFF",也就是说ID为"LIST"的块只能是子块(SubChunk),他们和各个子块形成了复杂的RIFF文件结构。   RIFF数据域的的起始位置四个字节为类型码(Form Type),用于说明数据域的格式,比如WAV文件的类型码为"WAVE"。   "LIST"块的数据域的起始位置也有一个四字节类型码(List Type),用于说明LIST数据域的数据内容。比如,类型码为"INFO"时,其数据域可能包括"ICOP"、"ICRD"块,用于记录文件版权和创建时间信息。  以最简单的无损WAV格式文件为例,此时文件的音频数据部分为PCM,比较简单,重点在于WAV头部。一个典型的WAV文件头部长度为44字节,包含了采样率,通道数,位深等信息.   上表为典型的WAV头部格式,从0x00到0x2B总共44字节,从0x2C开始一直到文件末尾都是PCM音频数据。所以如果你已经知道了PCM的采样信息,那么可以直接跳过头部的解析,直接从0x2C开始读取PCM即可,但是对于另一些无损的WAV文件却是不行的。 有一些WAV的头部并不仅仅只有44个字节,比如通过FFmpge编码而来的WAV文件头部信息通常大于44个字节。这是因为根据WAV规范,其头部还支持携带附加信息,所以只按照44个字节的长度去解析WAV头部信息是不一定正确的,还需要考虑附加信息。那么如何知道一个WAV文件头部是否包含附加信息呢?   根据"fmt "子块长度来判断即可。 如果fmt SubChunk Size等于0x10(16),表示头部不包含附加信息,即WAV头部信息长度为44;如果等于0x12(18),则包含附加信息,此时头部信息长度大于44。 当WAV头部包含附加信息时,fmt SubChunk Size长度为18,并且紧随是另一个子块,这个包含了一些自定义的附加信息,接着往下才是"data"子块. 如果一个无损WAV文件头部包含了附加信息,那么PCM音频所在的位置就不确定了,但由于附加信息也是一个子块(SubChunk),根据RIFF规范,该子块也必然记录着其长度信息,所以我们还是有办法能够动态计算出其位置,下面是计算步骤: 判断fmt块长度是否为18。 如果fmt长度为18,那么必然从0x26位置开始为附加信息块,0x30-0x33位置记录着该子块长度。 根据步骤2获取的子块长度,假定为N(16进制),那么PCM音频信息开始位置为:0x34 + N + 8。   以上步骤仅为逻辑推理得出,未经验证,但大致遵循以上步骤,如有错误,欢迎指正。 WAV是微软公司开发的一种音频格式文件,用于保存Windows平台的音频信息资源,它符合资源互换文件格式(Resource Interchange File Format,RIFF)文件规范。标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!

WAV通常用来保存PCM格式的原始音频数据,所以通常被称为无损音频。但是严格意义上来讲,WAV也可以存储其它压缩格式的音频数据。

RIFF格式规范

[https://www.cnblogs.com/shibuliao/archive/2014/06/26/3809353.html] [https://www.cnblogs.com/tocy/p/RIFF_FILE.html] [https://en.wikipedia.org/wiki/Interchange_File_Format] Resource Interchange File Format(简称RIFF),资源交换文件格式,是一种按照标记区块存储数据(tagged chunks)的通用文件存储格式,多用于存储音频、视频等多媒体数据。Microsoft在windows下的AVI、ANI 、WAV等都是基于RIFF实现的。

RIFF是由Microsoft和IBM于1991年,在windows 3.1中引入的,作为windows 3.1默认的多媒体文件格式。RIFF是参考Interchange File Format来的,二者主要的区别是字节序大端、小端的问题。在基于IBM的80x86系列主机下,RIFF的字节序是小端的;而在IFF原有的格式中是按照大端存储整型数据的。 RIFF文件的基本构成单元是chunk。通常情况下一个chunk是指多媒体数据的一个基本逻辑单元,比如视频的一帧数据、音频的一帧数据等等。每个chunk包含以下三个字段:

FOURCC(四字节码),用于标识chunk ID或chunk 类型。 四字节整数,表示chunk中的数据域长度(Size)。 数据域(data Field)。 chunk是可以嵌套的。 包含在一个chunk中的chunk被称为subchunk。只有ID为“RIFF”或者“LIST”的chunk允许拥有subchunk。RIFF文件的第一个chunk的id必须是“RIFF”四字节码,也就是说id为“LIST”的chunk只能是subchunk。

“RIFF”chunk的数据域的起始位置是一个四字节码(称为Form Type,类型码),用于说明数据域的格式,比如“WAV”、“AVI”等。

“LIST”chunk的数据域的起始位置也有一个四字节码(称为List Type,类型码),用于说明LIST数据域的数据内容。比如,“LIST”chunk的list type为“INFO”时,其数据域可能包括“ICOP”、“ICRD”chunk,用于记录文件版权和创建时间信息。 RIFF文件中多次提到四字节码,Windows中提供了用于标识四字节码FOURCC,对于不足四个的ASCII码,在右侧补空格字符即可。比如多媒体输出输出函数中的mmioFOURCC,定义如下:

FOURCC mmioFOURCC( CHAR ch0, CHAR ch1, CHAR ch2, CHAR ch3 ); #define MAKEFOURCC(ch0, ch1, ch2, ch3) \ ((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) d_type); } closedir(dir); } extern char *Font_buff; void app_main(void) { /*初始化spiffs用于存放字体文件或者图片文件或者网页文件或者音频文件*/ ESP_LOGI(TAG, "Initializing SPIFFS"); esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = "storage", .max_files = 20, .format_if_mount_failed = false}; esp_err_t ret = esp_vfs_spiffs_register(&conf); if (ret != ESP_OK) { if (ret == ESP_FAIL) ESP_LOGE(TAG, "Failed to mount or format filesystem"); else if (ret == ESP_ERR_NOT_FOUND) ESP_LOGE(TAG, "Failed to find SPIFFS partition"); else ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); return; } /*显示spiffs里的文件列表*/ SPIFFS_Directory("/spiffs/"); // 初始化nvs用于存放wifi或者其他需要掉电保存的东西 ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); play_i2s_init();//初始化I2S播放 //audio_play(1);//播放第一个文件 xTaskCreate(usart0_rx_task, "usart0_rx_task", 1024 * 5, NULL, configMAX_PRIORITIES, NULL);//创建串口监听任务 #ifdef DEBUG //play_spiffs_name("all.wav");//播放all.wav for(int j=0;j0){ data[rxBytes]=0; //usart0_rx_buf_index += rxBytes;//其实每次只能接受一个字符,这里计算索引 //if (usart0_rx_buf_index >= 1 && data[usart0_rx_buf_index]=='\n'){ //data[rxBytes] = 0;//自动重置 ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, data); ESP_LOG_BUFFER_HEXDUMP(RX_TASK_TAG, data, rxBytes, ESP_LOG_INFO); //比较字符串 if (strstr((char *)data, "0") != NULL){ i2s_play(0); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "1") != NULL){ i2s_play(1); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "2") != NULL){ i2s_play(2); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "3") != NULL){ i2s_play(3); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "4") != NULL){ i2s_play(4); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "5") != NULL){ i2s_play(5); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "6") != NULL){ i2s_play(6); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "7") != NULL){ i2s_play(7); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "8") != NULL){ i2s_play(8); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "9") != NULL){ i2s_play(9); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "10") != NULL) { i2s_play(10); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "11") != NULL){ i2s_play(11); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "12") != NULL){ i2s_play(12); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "13") != NULL){ i2s_play(13); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "14") != NULL){ i2s_play(14); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "15") != NULL){ i2s_play(15); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "16") != NULL){ i2s_play(16); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "17") != NULL){ i2s_play(17); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "18") != NULL){ i2s_play(18); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "19") != NULL){ i2s_play(19); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "20") != NULL){ i2s_play(20); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "21") != NULL){ i2s_play(21); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "s") != NULL){//十 i2s_play(10); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "b") != NULL){//百 i2s_play(11); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "q") != NULL){//千 i2s_play(12); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "w") != NULL){//万 i2s_play(13); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "+") != NULL){//加 i2s_play(14); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "j") != NULL){//减 i2s_play(15); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "x") != NULL){//乘 i2s_play(16); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "c") != NULL){//除 i2s_play(17); usart0_rx_buf_index=0;//重置 } else if(strstr((char *)data, "-") != NULL){//负 for(int k=0;k0) }//end while(1) free(data); }

app_main.h

点击查看代码 #ifndef APP_MAIN_H #define APP_MAIN_H #include #define VERSION "0.9.0" typedef enum { WAIT_FOR_WAKEUP, WAIT_FOR_CONNECT, START_DETECT, START_RECOGNITION, START_ENROLL, START_DELETE, } en_fsm_state; extern en_fsm_state g_state; extern int g_is_enrolling; extern int g_is_deleting; #endif

file_manager.c

点击查看代码 // Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include "esp_err.h" #include "esp_log.h" #include "esp_vfs_fat.h" #include "driver/sdspi_host.h" #include "driver/spi_common.h" #include "sdmmc_cmd.h" #include "file_manager.h" #ifdef CONFIG_IDF_TARGET_ESP32 #include "driver/sdmmc_host.h" #endif static const char *TAG = "file manager"; static const char *partition_label = "audio"; #define FLN_MAX 512 // Pin mapping when using SPI mode. // With this mapping, SD card can be used both in SPI and 1-line SD mode. // Note that a pull-up on CS line is required in SD mode. #define PIN_NUM_MISO 0 #define PIN_NUM_MOSI 4 #define PIN_NUM_CLK 3 #define PIN_NUM_CS 8 bool flag_mount = false; static sdmmc_card_t *card; void sd_unmount(void) { if (flag_mount == true) { esp_vfs_fat_sdcard_unmount(MOUNT_POINT, card); ESP_LOGI(TAG, "Card unmounted"); } spi_bus_free(SPI3_HOST); } esp_err_t fm_sdcard_init(void) { esp_err_t ret; esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 5, .allocation_unit_size = 16 * 1024}; ESP_LOGI(TAG, "Initializing SD card"); ESP_LOGI(TAG, "Using SPI peripheral"); sdmmc_host_t host = SDSPI_HOST_DEFAULT(); spi_bus_config_t bus_cfg = { .mosi_io_num = PIN_NUM_MOSI, .miso_io_num = PIN_NUM_MISO, .sclk_io_num = PIN_NUM_CLK, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 4000, }; ret = spi_bus_initialize(SPI3_HOST, &bus_cfg, SPI_DMA_CH_AUTO); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize bus."); return ESP_FAIL; } sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); slot_config.gpio_cs = 8; slot_config.host_id = SPI3_HOST; ret = esp_vfs_fat_sdspi_mount(MOUNT_POINT, &host, &slot_config, &mount_config, &card); if (ret != ESP_OK) { flag_mount = false; if (ret == ESP_FAIL) { ESP_LOGE(TAG, "Failed to mount filesystem. "); } else { ESP_LOGE(TAG, "Failed to initialize the card (%s). " "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret)); } return ESP_FAIL; } ESP_LOGW(TAG, "Success to mount filesystem. "); flag_mount = true; return ESP_OK; } esp_err_t fm_spiffs_init(void) { esp_err_t ret; ESP_LOGI(TAG, "Initializing SPIFFS"); esp_vfs_spiffs_conf_t conf = { .base_path = MOUNT_POINT, .partition_label = partition_label, .max_files = 5, // This decides the maximum number of files that can be created on the storage .format_if_mount_failed = false}; ret = esp_vfs_spiffs_register(&conf); if (ret != ESP_OK) { if (ret == ESP_FAIL) { ESP_LOGE(TAG, "Failed to mount or format filesystem"); } else if (ret == ESP_ERR_NOT_FOUND) { ESP_LOGE(TAG, "Failed to find SPIFFS partition"); } else { ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); } return ESP_FAIL; } size_t total = 0, used = 0; ret = esp_spiffs_info(partition_label, &total, &used); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret)); return ESP_FAIL; } ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used); return ESP_OK; } static void TraverseDir(char *direntName, int level, int indent) { DIR *p_dir = NULL; struct dirent *p_dirent = NULL; p_dir = opendir(direntName); if (p_dir == NULL) { printf("opendir error\n"); return; } while ((p_dirent = readdir(p_dir)) != NULL) { char *backupDirName = NULL; if (p_dirent->d_name[0] == '.') { continue; } int i; for (i = 0; i < indent; i++) { // printf("|"); printf(" "); } printf("|--- %s", p_dirent->d_name); /* Itme is a file */ if (p_dirent->d_type == DT_REG) { int curDirentNameLen = strlen(direntName) + strlen(p_dirent->d_name) + 2; //prepare next path backupDirName = (char *)malloc(curDirentNameLen); struct stat *st = NULL; st = malloc(sizeof(struct stat)); if (NULL == backupDirName || NULL == st) { goto _err; } memset(backupDirName, 0, curDirentNameLen); memcpy(backupDirName, direntName, strlen(direntName)); strcat(backupDirName, "/"); strcat(backupDirName, p_dirent->d_name); int statok = stat(backupDirName, st); if (0 == statok) { printf("[%dB]\n", (int)st->st_size); } else { printf("\n"); } free(backupDirName); backupDirName = NULL; } else { printf("\n"); } /* Itme is a directory */ if (p_dirent->d_type == DT_DIR) { int curDirentNameLen = strlen(direntName) + strlen(p_dirent->d_name) + 2; //prepare next path backupDirName = (char *)malloc(curDirentNameLen); if (NULL == backupDirName) { goto _err; } memset(backupDirName, 0, curDirentNameLen); memcpy(backupDirName, direntName, curDirentNameLen); strcat(backupDirName, "/"); strcat(backupDirName, p_dirent->d_name); if (level > 0) { TraverseDir(backupDirName, level - 1, indent + 1); } free(backupDirName); backupDirName = NULL; } } _err: closedir(p_dir); } void fm_print_dir(char *direntName, int level) { printf("Traverse directory %s\n", direntName); TraverseDir(direntName, level, 0); printf("\r\n"); } const char *fm_get_basepath(void) { return MOUNT_POINT; } esp_err_t fm_file_table_create(char ***list_out, uint16_t *files_number, const char *filter_suffix) { DIR *p_dir = NULL; struct dirent *p_dirent = NULL; p_dir = opendir(MOUNT_POINT); if (p_dir == NULL) { ESP_LOGE(TAG, "opendir error"); return ESP_FAIL; } uint16_t f_num = 0; while ((p_dirent = readdir(p_dir)) != NULL) { if (p_dirent->d_type == DT_REG) { f_num++; } } rewinddir(p_dir); *list_out = calloc(f_num, sizeof(char *)); if (NULL == (*list_out)) { goto _err; } for (size_t i = 0; i < f_num; i++) { (*list_out)[i] = malloc(FLN_MAX); if (NULL == (*list_out)[i]) { ESP_LOGE(TAG, "malloc failed at %d", i); fm_file_table_free(list_out, f_num); goto _err; } } uint16_t index = 0; while ((p_dirent = readdir(p_dir)) != NULL) { if (p_dirent->d_type == DT_REG) { if (NULL != filter_suffix) { if (strstr(p_dirent->d_name, filter_suffix)) { strncpy((*list_out)[index], p_dirent->d_name, FLN_MAX - 1); (*list_out)[index][FLN_MAX - 1] = '\0'; index++; } } else { strncpy((*list_out)[index], p_dirent->d_name, FLN_MAX - 1); (*list_out)[index][FLN_MAX - 1] = '\0'; index++; } } } (*files_number) = index; closedir(p_dir); return ESP_OK; _err: closedir(p_dir); return ESP_FAIL; } esp_err_t fm_file_table_free(char ***list, uint16_t files_number) { for (size_t i = 0; i < files_number; i++) { free((*list)[i]); } free((*list)); return ESP_OK; } const char *fm_get_filename(const char *file) { const char *p = file + strlen(file); while (p > file) { if ('/' == *p) { return (p + 1); } p--; } return file; } size_t fm_get_file_size(const char *filepath) { struct stat siz = {0}; stat(filepath, &siz); return siz.st_size; }

file_manager.h

点击查看代码 #ifndef _IOT_FILE_MANAGER_H_ #define _IOT_FILE_MANAGER_H_ #include "esp_err.h" #include "esp_spiffs.h" #include "esp_vfs.h" #ifdef __cplusplus extern "C" { #endif #define MOUNT_POINT "/sdcard" esp_err_t fm_sdcard_init(void); esp_err_t fm_spiffs_init(void); void fm_print_dir(char *direntName, int level); const char *fm_get_basepath(void); const char *fm_get_filename(const char *file); size_t fm_get_file_size(const char *filepath); esp_err_t fm_file_table_create(char ***list_out, uint16_t *files_number, const char *filter_suffix); esp_err_t fm_file_table_free(char ***list,uint16_t files_number); void sd_unmount(void); #ifdef __cplusplus } #endif #endif

mp3_player.c

点击查看代码 /* * @Descripttion : 通过串口点播数字音频 * @version : * @Author : Kevincoooool * @Date : 2021-05-25 09:20:06 * @LastEditors: qsbye * @LastEditTime: 2023-06-27 12:00:18 * @FilePath: mp3_player.c */ #include "mp3_player.h" #include "file_manager.h" #include "driver/i2s.h" #include "esp_log.h" #include "esp_spiffs.h" #include "mp3dec.h" #define TAG "WAV_PLAYER" #define I2S_NUM 0 /* 录音I2S初始化 */ void record_i2s_init(void) { i2s_config_t i2s_config = { .mode = I2S_MODE_MASTER | I2S_MODE_RX, // the mode must be set according to DSP configuration .sample_rate = 16000, // must be the same as DSP configuration .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // must be the same as DSP configuration .bits_per_sample = 32, // must be the same as DSP configuration .communication_format = I2S_COMM_FORMAT_STAND_I2S, .dma_buf_count = 2, .dma_buf_len = 300, .intr_alloc_flags = ESP_INTR_FLAG_LOWMED, .bits_per_chan = I2S_BITS_PER_SAMPLE_16BIT}; i2s_pin_config_t pin_config = { .mck_io_num = -1, .bck_io_num = IIS_SCLK, // IIS_SCLK .ws_io_num = IIS_LCLK, // IIS_LCLK .data_out_num = -1, // IIS_DSIN .data_in_num = IIS_DOUT // IIS_DOUT }; i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM, &pin_config); i2s_zero_dma_buffer(I2S_NUM); } /* 播放I2S初始化 */ void play_i2s_init(void) { i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 36000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LOWMED, .dma_buf_count = 2, .dma_buf_len = 300, .bits_per_chan = I2S_BITS_PER_SAMPLE_16BIT}; i2s_pin_config_t pin_config = { .mck_io_num = -1, .bck_io_num = IIS_SCLK, // IIS_SCLK .ws_io_num = IIS_LCLK, // IIS_LCLK .data_out_num = IIS_DSIN, // IIS_DSIN .data_in_num = -1 // IIS_DOUT }; i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM, &pin_config); i2s_zero_dma_buffer(I2S_NUM); } /* 卸载I2S驱动 */ void all_i2s_deinit(void) { i2s_driver_uninstall(I2S_NUM); vTaskDelay(10); } char path_buf[256] = {0}; char **g_file_list = NULL; uint16_t g_file_num = 0; bool playing = false; typedef struct { // The "RIFF" chunk descriptor uint8_t ChunkID[4]; int32_t ChunkSize; uint8_t Format[4]; // The "fmt" sub-chunk uint8_t Subchunk1ID[4]; int32_t Subchunk1Size; int16_t AudioFormat; int16_t NumChannels; int32_t SampleRate; int32_t ByteRate; int16_t BlockAlign; int16_t BitsPerSample; // The "data" sub-chunk uint8_t Subchunk2ID[4]; int32_t Subchunk2Size; } wav_header_t; esp_err_t play_wav(const char *filepath) { all_i2s_deinit();//先把i2s的驱动卸载掉 因为在这之前是把i2s初始化为录音了 现在要播放 公用的引脚就不能播放 需要卸载掉重新初始化为播放模式 play_i2s_init();//初始化播放模式的i2s FILE *fd = NULL; struct stat file_stat; if (stat(filepath, &file_stat) == -1)//先找找这个文件是否存在 { ESP_LOGE(TAG, "Failed to stat file : %s", filepath); //all_i2s_deinit(); //record_i2s_init(); return ESP_FAIL;//如果不存在就继续录音 } ESP_LOGI(TAG, "file stat info: %s (%ld bytes)...", filepath, file_stat.st_size); fd = fopen(filepath, "r"); if (NULL == fd) { ESP_LOGE(TAG, "Failed to read existing file : %s", filepath); //all_i2s_deinit(); //record_i2s_init(); return ESP_FAIL; } const size_t chunk_size = 4096; uint8_t *buffer = malloc(chunk_size); if (NULL == buffer) { ESP_LOGE(TAG, "audio data buffer malloc failed"); //all_i2s_deinit(); //record_i2s_init(); fclose(fd); return ESP_FAIL; } /** * read head of WAV file */ wav_header_t wav_head; int len = fread(&wav_head, 1, sizeof(wav_header_t), fd);//读取wav文件的文件头 if (len 0 i2s_set_clk(0, wav_head.SampleRate, wav_head.BitsPerSample, 1);//根据该wav文件的各种参数来配置一下i2S的clk 采样率等等 ESP_LOGI(TAG, "write data"); do { /* Read file in chunks into the scratch buffer */ len = fread(buffer, 1, chunk_size, fd); if (len 0 i2s_write(0, buffer, len, &cnt, 1000 / portTICK_PERIOD_MS);//输出数据到I2S 就实现了播放 write_num += len; } while (1); fclose(fd); ESP_LOGI(TAG, "File reading complete, total: %d bytes", write_num); all_i2s_deinit(); // record_i2s_init(); return ESP_OK; } void play_sdfile_name(char *file_name) { playing = true; fm_sdcard_init(); fm_print_dir("/sdcard", 2); fm_file_table_create(&g_file_list, &g_file_num, ".WAV"); for (size_t i = 0; i < g_file_num; i++) { ESP_LOGI(TAG, "have file [%d:%s]", i, g_file_list[i]); } if (0 == g_file_num) { ESP_LOGW(TAG, "Can't found any wav file in sdcard!"); all_i2s_deinit(); record_i2s_init(); sd_unmount(); playing = false; return; } sprintf(path_buf, "%s/%s", "/sdcard", file_name); play_wav(path_buf); sd_unmount(); fm_file_table_free(&g_file_list, g_file_num); record_i2s_init(); vTaskDelay(10); playing = false; } void play_spiffs_name(char *file_name) { playing = true; sprintf(path_buf, "%s/%s", "/spiffs/voice", file_name); play_wav(path_buf); //record_i2s_init(); vTaskDelay(10); playing = false; vTaskDelay(10); } #define SAMPLE_RATE (44100) #define I2S_NUM (0) #define WAVE_FREQ_HZ (100) #define PI (3.14159265) #define SAMPLE_PER_CYCLE (SAMPLE_RATE / WAVE_FREQ_HZ) xQueueHandle play_queue = NULL; /*!< aduio music list from spiffs*/ const char audio_list[2][64] = { "/spiffs/apple-tosk.mp3", "/spiffs/distance.mp3", // "/spiffs/longest_movie.mp3", }; enum { AUDIO_STOP = 0, AUDIO_PLAY, AUDIO_NEXT, AUDIO_LAST }; int play_flag = AUDIO_STOP; int audio_play_index = 0; void aplay_mp3(const char *path) { ESP_LOGI(TAG, "start to decode %s", path); HMP3Decoder hMP3Decoder; MP3FrameInfo mp3FrameInfo; unsigned char *readBuf = malloc(MAINBUF_SIZE); if (readBuf == NULL) { ESP_LOGE(TAG, "readBuf malloc failed"); return; } short *output = malloc(1153 * 4); if (output == NULL) { free(readBuf); ESP_LOGE(TAG, "outBuf malloc failed"); } hMP3Decoder = MP3InitDecoder(); if (hMP3Decoder == 0) { free(readBuf); free(output); ESP_LOGE(TAG, "memory is not enough.."); } int samplerate = 0; i2s_zero_dma_buffer(0); FILE *mp3File = fopen(path, "rb"); if (mp3File == NULL) { MP3FreeDecoder(hMP3Decoder); free(readBuf); free(output); ESP_LOGE(TAG, "open file failed"); } char tag[10]; int tag_len = 0; int read_bytes = fread(tag, 1, 10, mp3File); if (read_bytes == 10) { if (memcmp(tag, "ID3", 3) == 0) { tag_len = ((tag[6] & 0x7F) 100) // vol = 100; ESP_LOGI(TAG, "设置音量(%d)", vol); // es8311_set_voice_volume(vol); // xTaskCreate(audio_task, "audio_task", 4096, NULL, 5, NULL); return 0; } // void audio_setting_vol(uint32_t vol) // { // if (vol > 100) // vol = 100; // es8311_set_voice_volume(vol); // }

mp3_player.h

点击查看代码 #ifndef _wav_player_h_ #define _wav_player_h_ #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "freertos/semphr.h" #include "esp_err.h" #include "esp_log.h" #define IIS_SCLK 16 #define IIS_LCLK 7 #define IIS_DSIN 6 #define IIS_DOUT 15 extern bool playing; esp_err_t play_wav(const char *filepath); void play_sdfile_name(char *file_name); void play_spiffs_name(char *file_name); void play_i2s_init(void); void audio_play(uint8_t index); #ifdef __cplusplus } #endif 编译&烧录

编译例程

export TEMP=/Users/workspace/Desktop/projects/计算器/software/ksdiy_audio/ alias esp-idf='docker run --rm --privileged -v $TEMP:/project -w /project -it espressif/idf:release-v4.4 bash -c' esp-idf "cd '/project/22.mp3_player' && idf.py build"

烧录例程

export TEMP=/Users/workspace/Desktop/projects/计算器/software/ksdiy_audio/22.mp3_player/build #擦除原有程序(改成自己的串口号,可能要sudo) ls /dev/cu* esptool.py -h esptool.py --chip auto --port /dev/cu.usbmodem1301 erase_flash #烧入bin文件(注意匹配你的 bin文件名、串口号、flash大小与速率、传输波特率等) ##22.mp3_player esptool.py --chip auto --port /dev/cu.usbmodem1201 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 8MB 0x0000 $TEMP/bootloader/bootloader.bin 0x10000 $TEMP/hello-world.bin 0x8000 $TEMP/partition_table/partition-table.bin 0x302000 $TEMP/storage.bin #单独烧录storage esptool.py --chip auto --port /dev/cu.usbmodem1301 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 8MB 0x573000 $TEMP/storage.bin 效果

在系统调试的过程中,首先确定使用了电脑的哪一个串口。插上 USB 线后, 使用ls /dev/cu*命令查看串口,或者使用串口助手扫描串口。打开串口前,要注意 配置串口参数为115200,8,1,none.串口接收字符后播放对应音频。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3