esp32笔记[5]-基于I2S协议实现音频播放

发布时间 2023-07-17 05:10:25作者: qsBye

摘要

基于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) << 8) |  \ 
    ((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24 ));

也可使用mmioStringToFOURCC函数将字符串转化为四字节码。

如果不关心RIFF文件负载内容,可按照普通文件读写RIFF文件。

可使用mmioCreateChunk函数、mmioAscend函数、mmioDescend函数等读写RIFF文件、移动文件指针等。详细使用建议参考MSDN上相关内容。(Multimedia File I/O Services).

I2S协议

I2S 和 I2C 一样都是由飞利浦于上世纪八十年代推出的经典接口,于 1996 年定版,专门传输芯片之间的数字音频数据,主要用于 Codec、Audio PA、DSP 等。

标准I2S有3个主要信号: 串行时钟BCLK,帧时钟LRCLK,串行数据SDATA。串行时钟BCLK也叫位时钟,即对应数字音频的每一位数据。帧时钟LRCLK用于切换左右声道的数据。LRCLK为“1”表示正在传输的是右声道的数据,为“0”则表示正在传输的是左声道的数据,LRCLK的频率等于采样频率。串行数据SDATA就是用二进制补码表示的音频数据。
I2S是比较简单的数字接口协议,没有地址或设备选择机制。在I2S总线上,只能同时存在一个主设备和发送设备。主设备可以是发送设备,也可以是接收设备,或是协调发送设备和接收设备的其他控制设备。在I2S系统中,提供时钟(SCK和WS)的设备为主设备。
I2S包括两个声道(Left/Right)的数据,在主设备发出声道选择/字选择(WS)控制下进行左右声道数据切换。通过增加I2S接口的数目或其它I2S设备可以实现多声道(Multi-Channels)应用。

在I2S传输协议中,数据信号、时钟信号以及控制信号是分开传输的。I2S协议只定义三根信号线:时钟信号SCK、数据信号SD和左右声道选择信号WS。

时钟信号: Serial Clock
SCK是模块内的同步信号,从模式时由外部提供,主模式时由模块内部自己产生。不同厂家的芯片型号,时钟信号叫法可能不同,也可能称BCLK/Bit Clock或SCL/Serial Clock

数据信号: Serial Data
SD是串行数据,在I2S中以二进制补码的形式在数据线上传输。在WS变化后的第一个SCK脉冲,先传输最高位(MSB, Most Significant Bit)。先传送MSB是因为发送设备和接收设备的字长可能不同,当系统字长比数据发送端字长长的时候,数据传输就会出现截断的现象/Truncated,即如果数据接收端接收的数据位比它规定的字长长的话,那么规定字长最低位(LSB: Least Significant Bit)以后的所有位将会被忽略。如果接收的字长比它规定的字长短,那么空余出来的位将会以0填补。通过这种方式可以使音频信号的最高有效位得到传输,从而保证最好的听觉效果。
左右声道选择信号 Word Select
WS是声道选择信号,表明数据发送端所选择的声道。当:

√ WS=0,表示选择左声道

√ WS=1,表示选择右声道

WS也称帧时钟,即LRCLK/Left RightClock。WS频率等于声音的采样率。WS既可以在SCK的上升沿,也可以在SCK的下降沿变化。从设备在SCK的上升沿采样WS信号。数据信号MSB在WS改变后的第二个时钟(SCK)上升沿有效(即延迟一个SCK),这样可以让从设备有足够的时间以存储当前接收的数据,并准备好接收下一组数据。

制作WAV音频并存储到ESP32的SPI FLASH中

PCM格式信息:
- 声道:1
- 采样率16000
- 每秒数据字节数:32000
- 每个采样率所需字节数:2
- 单个采样位深:16
- 小端字节序:little-endian

制作WAV音频

[https://www.haah.net/?p=2463]
[https://pan.baidu.com/s/1gfaHBwB]
wav 语音文件使用讯飞 TTS 软件 生成。wav文件由于比较大(20KB 至 100KB),因而使用 SPIFFS 文件系统存储在单片机SPI FLASH中。

科大讯飞5.0云龙绿化特别版

  1. 高质量语音——将输入文本实时转换为流畅、清晰、自然和具有表现力的语音数据;
  2. 多语种服务——整合了多语种语音合成引擎,可提供中文、中英文混读、英文、广东话的语音合成服务;
  3. 高精度文本分析技术——保证了对文本中未登录词(如地名)、多音字、特殊符号(如标点、数字)、韵律短语等智能分析和处理;
  4. 多字符集支持——支持输入GB2312、GBK、Big5、Unicode和UTF-8等多种字符集,普通文本和带有CSSML标注等多种格式的文本信息;
  5. 多种数据输出格式——支持输出多种采用率的线性Wav,A/U率Wav和Vox等格式的语音数据;
  6. 灵活的接口——提供了标准接口、简单接口、COM接口、SAPI接口,便于在多种环境下进行系统的集成;
  7. 语音调整功能——开发接口提供了音量、语速、音高等多种合成参数的动态调整功能;
  8. 配置和管理工具——合成引擎提供了统一进行配置和管理的工具,完成了全局参数配置、用户词典、用户规则、定制资源包管理等功能;
  9. 效果优化——合成引擎提供了以定制资源包和CSSML为代表的多种针对实际应用环境进行合成效果优化的方法;
  10. 一致的访问方式——能以Client/Server方式访问远程的语音合成服务,并且提供与本地调用相同的开发接口,实现了完全透明的访问;
  11. 动态负载均衡——提供了动态负载均衡模块,以对用户透明的方式动态调配多台语音合成服务器的资源;
  12. 背景音和预录音——合成系统还提供了背景音和预录音的功能 ,满足用户不同场合的应用和个性化需求。

音库编号 发音人 发音风格 支持语种 支持采样率

  1. 小静 中年女声,音质平和,风格轻柔沉稳 中文及中英混读 6K/8k/11k/16k
  2. 小燕 青年女声,音质清脆,风格轻松活泼 中文及中英混读 6K/8k/11k/16k
  3. 小美 青年女声,音质清脆,风格亲切宜人 粤语及粤英混读 6K/8k/11k/16k
  4. 小宇 中年男声,音质淳厚,风格沉稳柔和 中英混读及纯英文 6K/8k/11k/16k
  5. Sherri 青年女声,音质平和,风格轻柔平稳 英文 6K/8k/11k/16k
  6. 小倩 青年女声,音质甜美,风格轻快活泼 中文及中英混读 6K/8k/11k/16k
  7. 小琳 青年女声,音质清脆,风格亲切宜人 台湾国语及中英混读 6K/8k/11k/16k

这个软件应该算是高科技了,把文本变成语音,合成音质可媲美真人朗读,基本达到了播音员的效果。语音软件有很多种,那些轻量级的、体积小的语音软件一般都是电脑合成语音或联网读取语音库,但本软件自带多种16K高音质语音库,所以体积有7G之大。本版采用程序虚拟化技术封装成绿色便携版,无须绿化安装,可在移动硬盘、U盘内直接运行。已整合破解程序,集成安装了小燕、小美、小宇、Sherri、小倩、小琳真人语音库,小静语音库网上未见到,故未集成。一般常用小燕、小宇语音库,也是音质、音效最好的两个语音库。本版完美解决了WIN7、WIN8、32位、64位系统下原程序很难正常运行的问题。但CSSML编辑器在WIN7以上系统,音频设备无法打开(XP下正常)的问题,暂无法解决。中科大讯飞语音合成系统原版安装破解步骤繁琐,网上居然还有专门的安装教程,这个软件以前有云龙绿化版,但对WIN7系统支持不太好,语音库也很少。制作本绿色便携版的目的就是想化繁为简,并延续这个软件的生命,在新系统上能正常运行。

制作SPIFFS分区

[https://www.cnblogs.com/kerwincui/p/13955250.html]
[https://docs.espressif.com/projects/esp-idf/zh_CN/v4.4.3/esp32s3/api-reference/storage/spiffs.html]
[https://github.com/espressif/esp-idf/blob/v4.4.3/components/spiffs/spiffsgen.py]
[https://blog.csdn.net/liahfdsaf/article/details/119062474]
SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能。

export TEMP=/Users/workspace/Desktop/projects/ESP32_S3_Screen_ksdiy/files/esp32-S3_DEMO_1213
alias esp-idf='docker run --rm --privileged -v $TEMP:/project -w /project -it espressif/idf:release-v4.4 bash -c'
esp-idf "cd '/project/16.Test' && python /opt/esp/idf/components/spiffs/spiffsgen.py 0x180000 /project/16.Test/spiffs /project/16.Test/spiffs.bin"
#参数介绍
python spiffsgen.py <image_size> <base_dir> <output_file>

参数(必选)说明如下:

  • image_size:分区大小,用于烧录生成的 SPIFFS 镜像;
  • base_dir:创建 SPIFFS 镜像的目录;
  • output_file:SPIFFS 镜像输出文件。

用户可以在命令行或脚本中手动单独调用 spiffsgen.py,也可以直接从构建系统调用 spiffs_create_partition_image 来使用 spiffsgen.py。

以生成100k的文件系统为例,执行以下命令,生成spiffs.bin文件,这就是spiffs文件系统。其大小0x19000就是100k。
计算方法:
100k=100*1024 Byte=0x19000 Byte

python spiffsgen.py 0x19000 root spiffs.bin

SPIFFS分区大小为1.5MB,则102410241.5=1572864 Byte=0x180000 Byte
计算烧录时的偏移地址:0x10000 + 5500K + 16K 约等于 0x573000
flasher_args.json

{
    "write_flash_args" : [ "--flash_mode", "dio",
                           "--flash_size", "detect",
                           "--flash_freq", "80m" ],
    "flash_settings" : {
        "flash_mode": "dio",
        "flash_size": "detect",
        "flash_freq": "80m"
    },
    "flash_files" : {
        "0x0" : "bootloader/bootloader.bin",
        "0x10000" : "hello-world.bin",
        "0x8000" : "partition_table/partition-table.bin",
        "0x573000" : "storage.bin"
    },
    "bootloader" : { "offset" : "0x0", "file" : "bootloader/bootloader.bin", "encrypted" : "false" },
    "app" : { "offset" : "0x10000", "file" : "hello-world.bin", "encrypted" : "false" },
    "partition-table" : { "offset" : "0x8000", "file" : "partition_table/partition-table.bin", "encrypted" : "false" },
    "storage" : { "offset" : "0x573000", "file" : "storage.bin", "encrypted" : "false" },
    "extra_esptool_args" : {
        "after"  : "hard_reset",
        "before" : "default_reset",
        "stub"   : true,
        "chip"   : "esp32s3"
    }
}

(ArduinoIDE)ESP32的串口接收中断

[https://blog.csdn.net/weixin_43507946/article/details/97060854]
[https://www.icxbk.com/ask/detail/27692.html]
Serial.begin后就自动开启串口中断了,并不需要自己写什么,串口中断会将数据存入缓冲区,此时数据已经在ESP32上了,Serial.read是一个从缓冲区提取数据的方法。
ArduinoIDE的中断是通过事件实现的,名字叫SerialEvent,但是实际上Arduino的串口接收可以接收不定长数据,因此不需要用到中断。

String inputString = "";      // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

void setup() {
  // initialize serial:
  Serial.begin(9600);
  // reserve 200 bytes for the inputString:
  inputString.reserve(200);
}

void loop() {
  // print the string when a newline arrives:
  if (stringComplete) {
    Serial.println(inputString);
    // clear the string:
    inputString = "";
    stringComplete = false;
  }
}

/*
  SerialEvent occurs whenever a new data comes in the hardware serial RX. This
  routine is run between each time loop() runs, so using delay inside loop can
  delay response. Multiple bytes of data may be available.
*/
void serialEvent() {
  while (Serial.available()) {
    // get the new byte:
    char inChar = (char)Serial.read();
    // add it to the inputString:
    inputString += inChar;
    // if the incoming character is a newline, set a flag so the main loop can
    // do something about it:
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

实现

代码目录结构

./src
|
├── app_main.c 
├── app_main.h 
├── mp3_player.c 
├── mp3_player.h 
├── file_manager.c 
├── file_manager.h

./spiffs
├── spiffs.txt
└── voice
    ├── all.wav
    ├── ba.wav
    ├── bai.wav
    ├── cheng.wav
    ├── chu.wav
    ├── dengyu.wav
    ├── dian.wav
    ├── er.wav
    ├── fenzhi.wav
    ├── fu.wav
    ├── jia.wav
    ├── jian.wav
    ├── jiu.wav
    ├── ling.wav
    ├── liu.wav
    ├── qi.wav
    ├── qian.wav
    ├── san.wav
    ├── shi.wav
    ├── si.wav
    ├── wan.wav
    ├── wu.wav
    └── yi.wav

代码

app_main.c

点击查看代码
/*
 * @Descripttion :  通过串口点播数字音频
 * @version      :  
 * @Author       : Kevincoooool
 * @Date         : 2021-05-25 09:20:06
 * @LastEditors: qsbye
 * @LastEditTime: 2023-06-27 12:00:18
 * @FilePath: appmain.c
 */

/*
ESP32-KSDIY的扬声器测试
音频与数组对照:
- voice_ling:零:0
- voice_yi:一:1
- voice_er:二:2
- voice_san:三:3
- voice_si:四:4
- voice_wu:五:5
- voice_liu:六:6
- voice_qi:七:7
- voice_ba:八:8
- voice_jiu:九:9
- voice_shi:十:10
- voice_bai:百:11
- voice_qian:千:12
- voice_wan:万:13
- voice_jia:加:14
- voice_jian:减:15
- voice_cheng:乘:16
- voice_chu:除:17
- voice_fu:负:18
- voice_dian:点:19
- voice_dengyu:等于:20
- voice_fenzhi:分之:21
PCM格式信息:
- 声道:1
- 采样率:16000
- 每秒数据字节数:32000
- 每个采样率所需字节数:2
- 单个采样位深:16
- 小端字节序:little-endian
ESP32烧录方式:
- SPI Flash Speed:40MHz
- SPI Mode:QIO
- 速率:921600
- 地址:0x0000(完整bin)
- 理论上两个C口都能烧录
串口引脚:
- USART0_RX:44
- USART0_TX:43
- 开发板使用J1接口才有串口输出
*/
#include <stdio.h>
#include "string.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_freertos_hooks.h"

#include "lv_examples/src/lv_demo_widgets/lv_demo_widgets.h"
#include "lv_examples/src/lv_demo_music/lv_demo_music.h"
#include "lv_examples/src/lv_demo_benchmark/lv_demo_benchmark.h"
#include "lvgl_helpers.h"

#include "esp_vfs.h"
#include "esp_spiffs.h"

#include "driver/gpio.h"
#include "driver/uart.h"
#include "driver/rmt.h"

#include "nvs_flash.h"
#include "app_main.h"
#include "mp3_player.h"

#define TAG "ESP32S3"
#define USART1_TXD_PIN (GPIO_NUM_43) //IO43
#define USART1_RXD_PIN (GPIO_NUM_44) //IO44

#define DEBUG 1

#ifdef uint8_t 
#define uint8_t unsigned char
#endif

/*开始全局变量*/
uint8_t voice_num_isr=22;//volatile用于ISR,串口调用语音播放
uint8_t voice_num_string_isr;//缓存接收到的ascii,下一步就是转换为数字
uint8_t voice_num_rec_complete=0;//接收完成标识位
static const int USART0_RX_BUF_SIZE = 1000;//usart0串口接收缓存区大小
int usart0_rx_buf_index=0;//usart0缓存区索引
/*结束全局变量*/

/*开始函数原型*/
void i2s_play(uint8_t voice_num);//i2s播放音频
void usart0_interrupt_callback();//串口0中断回调
static void usart0_rx_task(void *arg);//串口0接收任务
/*结束函数原型*/

/*用定时器给LVGL提供时钟*/
static void lv_tick_task(void *arg)
{
	(void)arg;
	lv_tick_inc(10);
}

SemaphoreHandle_t xGuiSemaphore;

static void gui_task(void *arg)
{
	xGuiSemaphore = xSemaphoreCreateMutex();
	lv_init(); // lvgl内核初始化

	lvgl_driver_init(); // lvgl显示接口初始化
	//申请两个buffer来给lvgl刷屏用  
	/*外部PSRAM方式*/
	// lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc(DISP_BUF_SIZE * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
	// lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(DISP_BUF_SIZE * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);

	/*内部DMA方式*/
	lv_color_t *buf1 = heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
	lv_color_t *buf2 = heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);

	/*静态数组方式*/
	// static lv_color_t buf1[DISP_BUF_SIZE];
	// static lv_color_t buf2[DISP_BUF_SIZE];
	static lv_disp_buf_t disp_buf;
	uint32_t size_in_px = DISP_BUF_SIZE;
	lv_disp_buf_init(&disp_buf, buf1, buf2, size_in_px);

	lv_disp_drv_t disp_drv;
	lv_disp_drv_init(&disp_drv);
	disp_drv.flush_cb = disp_driver_flush;
	disp_drv.buffer = &disp_buf;
	lv_disp_drv_register(&disp_drv);
	/*触摸屏输入接口配置*/
	lv_indev_drv_t indev_drv;
	lv_indev_drv_init(&indev_drv);
	indev_drv.read_cb = touch_driver_read;
	indev_drv.type = LV_INDEV_TYPE_POINTER;
	lv_indev_drv_register(&indev_drv);

	// esp_register_freertos_tick_hook(lv_tick_task);
	/* 创建一个定时器中断来进入 lv_tick_inc 给lvgl运行提供心跳 这里是10ms一次 主要是动画运行要用到 */
	const esp_timer_create_args_t periodic_timer_args = {
		.callback = &lv_tick_task,
		.name = "periodic_gui"};
	esp_timer_handle_t periodic_timer;
	ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
	ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 10 * 1000));

	// lv_demo_widgets();
	// lv_demo_music();
	// lv_demo_benchmark();
	// avi_player_load();

	while (1)
	{
		/* Delay 1 tick (assumes FreeRTOS tick is 10ms */
		vTaskDelay(pdMS_TO_TICKS(10));

		/* Try to take the semaphore, call lvgl related function on success */
		if (pdTRUE == xSemaphoreTake(xGuiSemaphore, portMAX_DELAY))
		{

			lv_task_handler();
			xSemaphoreGive(xGuiSemaphore);
		}
	}
}
/*显示spiffs的所有文件名*/
static void SPIFFS_Directory(char *path)
{
	DIR *dir = opendir(path);
	assert(dir != NULL);
	while (true)
	{
		struct dirent *pe = readdir(dir);
		if (!pe)
			break;
		ESP_LOGI(__FUNCTION__, "d_name=%s d_ino=%d d_type=%x", pe->d_name, pe->d_ino, pe->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;j<21;j++){
		i2s_play(j);
	}
	printf("playing all.wav!\n");
	#endif


	/*创建lvgl任务显示*/
	//xTaskCreatePinnedToCore(&gui_task, "lvgl task", 1024 * 5, NULL, 5, NULL, 1);
}

/*开始函数本体*/
/*
根据数字点播音频
*/
void i2s_play(uint8_t voice_num) {
  switch (voice_num) {
    case 0: {
      play_spiffs_name("ling.wav"); // 0
      break;
    }
    case 1: {
      play_spiffs_name("yi.wav"); // 1
      break;
    }
    case 2: {
      play_spiffs_name("er.wav"); // 2
      break;
    }
    case 3: {
      play_spiffs_name("san.wav"); // 3
      break;
    }
    case 4: {
      play_spiffs_name("si.wav"); // 4
      break;
    }
    case 5: {
      play_spiffs_name("wu.wav"); // 5
      break;
    }
    case 6: {
      play_spiffs_name("liu.wav"); // 6
      break;
    }
    case 7: {
      play_spiffs_name("qi.wav"); // 7
      break;
    }
    case 8: {
      play_spiffs_name("ba.wav"); // 8
      break;
    }
    case 9: {
      play_spiffs_name("jiu.wav"); // 9
      break;
    }
    case 10: {
      play_spiffs_name("shi.wav"); // 10
      break;
    }
    case 11: {
      play_spiffs_name("bai.wav"); // 11
      break;
    }
    case 12: {
      play_spiffs_name("qian.wav"); // 12
      break;
    }
    case 13: {
      play_spiffs_name("wan.wav"); // 13
      break;
    }
    case 14: {
      play_spiffs_name("jia.wav"); // 14
      break;
    }
    case 15: {
      play_spiffs_name("jian.wav"); // 15
      break;
    }
    case 16: {
      play_spiffs_name("cheng.wav"); // 16
      break;
    }
    case 17: {
      play_spiffs_name("chu.wav"); // 17
      break;
    }
    case 18: {
      play_spiffs_name("fu.wav"); // 18
      break;
    }
    case 19: {
      play_spiffs_name("dian.wav"); // 19
      break;
    }
    case 20: {
      play_spiffs_name("dengyu.wav"); // 20
      break;
    }
    case 21: {
      play_spiffs_name("fenzhi.wav"); // 21
      break;
    }
    default: {
      break;
    }
  }
}

/*
串口中断回调
*/
void usart0_interrupt_callback(){
	//pass
}

/*
串口0接收任务
*/
static void usart0_rx_task(void *arg){
    static const char *RX_TASK_TAG = "RX_TASK";
    esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO);

	//配置串口
	const uart_config_t uart_config = {
	.baud_rate = 115200,
	.data_bits = UART_DATA_8_BITS,
	.parity = UART_PARITY_DISABLE,
	.stop_bits = UART_STOP_BITS_1,
	.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
	.source_clk = UART_SCLK_APB,
	};
    uart_driver_install(UART_NUM_0, USART0_RX_BUF_SIZE * 2, 0, 0, NULL, 0);
    uart_param_config(UART_NUM_0, &uart_config);
    uart_set_pin(UART_NUM_0, 43, 44, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    uint8_t *data = (uint8_t *)malloc(USART0_RX_BUF_SIZE + 1); // 申请一块内存空间 与free()成对出现
	printf("malloc USART0_RX_BUF_SIZE ok!\n");
    while (1){
        int rxBytes = uart_read_bytes(UART_NUM_0, data, USART0_RX_BUF_SIZE, 1000 / portTICK_RATE_MS); // important 重要函数 接收
		if(rxBytes >0){
			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;k<10;k++){
				i2s_play(18);
				}
				usart0_rx_buf_index=0;//重置
			}
			else if(strstr((char *)data, ".") != NULL){//点
				i2s_play(19);
				usart0_rx_buf_index=0;//重置
			}
			else if(strstr((char *)data, "=") != NULL){//等于
				i2s_play(20);
				usart0_rx_buf_index=0;//重置
			}
			else if(strstr((char *)data, "/") != NULL){//分之
				i2s_play(21);
				usart0_rx_buf_index=0;//重置
			}
        //}end if rx_buf_index
      }//end if(rxBytes>0)
	}//end while(1)
    free(data);
}

app_main.h

点击查看代码
#ifndef APP_MAIN_H
#define APP_MAIN_H

#include <stdint.h>

#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 <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#include <ctype.h>
#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)
	{
		ESP_LOGE(TAG, "Read wav header failed");
		//all_i2s_deinit();
		//record_i2s_init();
		fclose(fd);
		return ESP_FAIL;
	}
	if (NULL == strstr((char *)wav_head.Subchunk1ID, "fmt") &&
		NULL == strstr((char *)wav_head.Subchunk2ID, "data"))
	{
		ESP_LOGE(TAG, "Header of wav format error");
		//all_i2s_deinit();
		//record_i2s_init();
		fclose(fd);
		return ESP_FAIL;
	}

	ESP_LOGI(TAG, "frame_rate=%d, ch=%d, width=%d", wav_head.SampleRate, wav_head.NumChannels, wav_head.BitsPerSample);
	/**
	 * read wave data of WAV file
	 */
	size_t write_num = 0;
	size_t cnt;
	ESP_LOGI(TAG, "set clock");
    //I2S:1->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)
		{
			break;
		}
        //I2S:1->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) << 21) | ((tag[7] & 0x7F) << 14) | ((tag[8] & 0x7F) << 7) | (tag[9] & 0x7F);
            // ESP_LOGI(TAG,"tag_len: %d %x %x %x %x", tag_len,tag[6],tag[7],tag[8],tag[9]);
            fseek(mp3File, tag_len - 10, SEEK_SET);
        }
        else
        {
            fseek(mp3File, 0, SEEK_SET);
        }
    }

    int bytesLeft = 0;
    unsigned char *readPtr = readBuf;
    play_flag = AUDIO_PLAY;

    while (1)
    {
        switch (play_flag)
        {
        case AUDIO_STOP:
        {
            while (!play_flag)
            {
                i2s_zero_dma_buffer(0);
                vTaskDelay(100 / portTICK_RATE_MS);
                goto stop;
            }
            break;
        }
        break;

        case AUDIO_PLAY:
        {
        }
        break;

        case AUDIO_NEXT:
        {
            if (audio_play_index < 3 - 1)
            {
                audio_play_index++;
            }
            else
            {
                audio_play_index = 0;
            }

            goto stop;
        }
        break;

        case AUDIO_LAST:
        {
            if (audio_play_index > 0)
            {
                audio_play_index--;
            }
            else
            {
                audio_play_index = 3 - 1;
            }

            goto stop;
        }
        break;
        }

        if (bytesLeft < MAINBUF_SIZE)
        {
            memmove(readBuf, readPtr, bytesLeft);
            int br = fread(readBuf + bytesLeft, 1, MAINBUF_SIZE - bytesLeft, mp3File);

            if ((br == 0) && (bytesLeft == 0))
            {
                break;
            }

            bytesLeft = bytesLeft + br;
            readPtr = readBuf;
        }

        int offset = MP3FindSyncWord(readPtr, bytesLeft);

        if (offset < 0)
        {
            ESP_LOGE(TAG, "MP3FindSyncWord not find");
            bytesLeft = 0;
            continue;
        }
        else
        {
            readPtr += offset;   /*!< data start point */
            bytesLeft -= offset; /*!< in buffer */
            int errs = MP3Decode(hMP3Decoder, &readPtr, &bytesLeft, output, 0);

            if (errs != 0)
            {
                ESP_LOGE(TAG, "MP3Decode failed ,code is %d ", errs);
                break;
            }

            MP3GetLastFrameInfo(hMP3Decoder, &mp3FrameInfo);

            if (samplerate != mp3FrameInfo.samprate)
            {
                samplerate = mp3FrameInfo.samprate;
                i2s_set_clk(0, samplerate, 16, mp3FrameInfo.nChans);
                ESP_LOGI(TAG, "mp3file info---bitrate=%d,layer=%d,nChans=%d,samprate=%d,outputSamps=%d", mp3FrameInfo.bitrate, mp3FrameInfo.layer, mp3FrameInfo.nChans, mp3FrameInfo.samprate, mp3FrameInfo.outputSamps);
            }

            size_t bytes_write = 0;
            i2s_write(0, (const char *)output, mp3FrameInfo.outputSamps * 2, &bytes_write, 100 / portTICK_RATE_MS);
            // rmt_write_items(0,(const char*)output,mp3FrameInfo.outputSamps*2, 1000 / portTICK_RATE_MS);
        }
    }

stop:
    i2s_zero_dma_buffer(0);
    MP3FreeDecoder(hMP3Decoder);
    free(readBuf);
    free(output);
    fclose(mp3File);

    ESP_LOGI(TAG, "end mp3 decode ..");
}

// static void audio_task(void *arg)
// {
//     /*!<  for 36Khz sample rates, we create 100Hz sine wave, every cycle need 36000/100 = 360 samples (4-bytes or 8-bytes each sample) */
//     /*!<  depend on bits_per_sample */
//     /*!<  using 6 buffers, we need 60-samples per buffer */
//     /*!<  if 2-channels, 16-bit each channel, total buffer is 360*4 = 1440 bytes */
//     /*!<  if 2-channels, 24/32-bit each channel, total buffer is 360*8 = 2880 bytes */
//     i2s_config_t i2s_config = {
//         .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX, /*!<  Only TX */
//         .sample_rate = SAMPLE_RATE,
//         .bits_per_sample = 16,
//         .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, /*!< 1-channels */
//         .communication_format = I2S_COMM_FORMAT_I2S,
//         .dma_buf_count = 6,
//         .dma_buf_len = 256,
//         .use_apll = true,
//         .tx_desc_auto_clear = true, /*!< I2S auto clear tx descriptor if there is underflow condition (helps in avoiding noise in case of data unavailability) */
//         .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_IRAM,
//     };
//     i2s_pin_config_t pin_config = {
//         .bck_io_num = I2S_SCLK,
//         .ws_io_num = I2S_LCLK,
//         .data_out_num = I2S_DOUT,
//         .data_in_num = I2S_DSIN /*!< Not used */
//     };

//     i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
//     i2s_set_pin(I2S_NUM, &pin_config);

//     unsigned int mp3_index = 0;
//     while (1)
//     {
//         if (xQueueReceive(play_queue, &mp3_index, portTICK_RATE_MS))
//         {
//             printf("play_queue: mp3_index = %u\n", mp3_index);

//             if (mp3_index > (AUDIO_MAX_PLAY_LIST - 1))
//                 printf("mp3_index: error\n");
//             else
//             {
//                 aplay_mp3(audio_list[mp3_index]);
//                 vTaskDelay(1000 / portTICK_RATE_MS);
//             }
//         }
//     }
// }
void audio_play(uint8_t index)
{
	printf("play_queue: mp3_index = %d\n", index);
	aplay_mp3(audio_list[index]);
}
int audio_init(uint32_t vol)
{
    // es8311_init(SAMPLE_RATE);
    
    // if (vol > 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 <stdio.h>
#include <string.h>
#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.串口接收字符后播放对应音频。