ESP32_软解mp3音频文件

这是课程视频的简化版文字稿,你可以在这里找到完整的课程视频

T型板的不足

之前T型开发板性能尚可,价格实惠,通用型强,适合基础课程的学习,但是它有以下制约:

  • 整体处理能力有限,难以处理复杂任务或多任务情景
  • 缺乏对USB-OTG的支持,难以进行鼠标,键盘等外设的扩展
  • 芯片IO接口较少,难以同时连接多种外设
  • 对复杂电路案例来说,搭建成本和可靠性均不能满足要求

USB-OTG允许 USB 设备充当主机或周边设备。这意味着USB-OTG设备可以连接到其他 USB 设备,而无需计算机或其他主机设备。

认识本课程所需的设备

  1. 基于ESP32-S3的多媒体开发平台
  • 图片展示
    01_00.png|325
  • 采用ESP32-S3-WROOM模块,内含8MB闪存与8MB内存
  • 配备8键无冲数字键盘,方便支持组合键的开发
  • 配有TF卡槽,方便存储空间拓展
  • 采用160x128 ST7735彩屏进行显示,具有屏幕触控功能
  • 采用两个USB-C接口,分别用与连接上位机和实现USB-OTG
  • 采用I2S实现音频输出,配备3.5mm音频输出接口(新版本的开发平台内置了扬声器)
  • 板载MPU-6050传感器,支持体感开发

ESP32是乐鑫科技开发的一系列低成本,低功耗的单片机微控制器,集成了Wi-Fi和双模蓝牙.它采用Tensilica Xtensa LX6双核和单核微处理器,内置无线开关,RF换衡器,功率放大器,低噪声接收放大器,滤波器和电源管理模块

ST7735是一款具有SPI接口的彩色TFT液晶显示驱动芯片,具有 128 x 160 像素的显示分辨率,可显示 16 位RGB颜色

MPU6050是一种集成了 3 轴加速度计和 3 轴陀螺仪的微型运动处理单元(IMU) .它是一种小型,低功耗的传感器,能方便的采集加速度和角速度数据,用于测量物体的运动

I2S是用于数字音频传输的串行总线标准,由三条线组成:数据线,时钟线和选择线.其是一种面向主从架构的总线:主设备负责产生时钟信号并将数据传输到从设备,从设备负责接收数据并将其转换为模拟音频信号

  1. 基于ESP32-S3的智能摄像头
  • 图片展示
    01_01.png|175
  • 采用ESP32-S3模块,并配备ST7735彩屏
  • 内置OV2640摄像头,便于开发CV,图传类应用
  • 内置TF卡槽和MPU6050六轴传感器
  • 基于拓展手柄可支持I2S音频输出,5D按键,INMP441数字麦克风
  • 配有7Pin拓展接口便于连接其他外设,用USB-C接口连接上位机

OV2640是一款 1632 x 1232 像素,1/4 英寸,低功耗 CMOS 图像传感器,可用于各种应用.包括机器视觉,物联网,无人机和智能家居,具有 50 帧/秒的视频帧率和 0.008 lux 的低光性能

INMP441是 TDK InvenSense 制造的一种高性能,低功耗,数字输出,全向 MEMS 麦克风.它采用 4.72 x 3.76 x 1 毫米的薄型表面贴装封装,具有可回流焊接兼容性,无灵敏度下降

配套软件系统的重要性

工作室定制了高度客制化的MicroPython固件,继承官方MicroPython特性的同时,还添加了以下支持:

  • MP3解码
  • FFT计算辅助
  • 摄像头支持(OV2640)
  • TFLite Runtime
  • 声音池
  • 软管线3D
  • 2D图像仿射变换
  • 帧缓冲缩放辅助
  • I2S音频录放
  • 数字图像处理
  • 颜色识别
  • USB键鼠和摄像头
    以上功能模块可以极大方便人工智能,2D/3D动画和音频等复杂功能的开发

FFT 是快速傅里叶变换的缩写,它是一种计算离散傅里叶变换 (DFT) 的有效算法.FFT有许多应用:包括信号处理,图像处理和信号合成

TensorFlow Lite 是 TensorFlow 的轻量级版本,用于在移动设备,嵌入式设备和物联网设备上运行机器学习模型

运行时(Runtime)是计算机程序运行生命周期内的一个重要阶段,它负责提供程序运行所需的各种服务,并确保程序能够正确执行

管线通常用于处理数据:例如一个图像处理管线可以将图像从原始格式转换为压缩格式,然后进行滤波和增强

背景知识

相较于WAV音频,MP3音频在同等条件下占用空间更小.但由于这种类型的文件存储的是编码压缩之后的音频数据,所以播放前需要进行解码.ESP32-S3的性能相较于T型板有了显著提升,可以进行音频解码操作

hzmp3 模块包含的工具方法

我们将用几个例子引入它们:

  1. 获取MP3文件信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import hzmp3

    data = open("test.mp3", "rb").read(2048) #by default #as an array

    file_size = os.path.getsize("test.mp3")

    info = hzmp3.getMp3Info(data, len(data), file_size)

    print("总时间长度:", info[0])
    print("第一帧数据字节数:", info[1])
    print("普通帧数据的字节数:", info[2])
    print("采样率:", info[3])
    print("声道型:", info[4]) #1 for stereo, 2 for mono
  2. 初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import hzmp3

    bck, ws, sd = "I2S的三个GPIO引脚编号"
    samplerRate = "待播放MP3的采样率"
    channels = "声道数"

    hzmp3.initMp3(bck, ws, sd, samplerRate, channels)

    #初始化解码器,I2S和工作在另一个核心的播放任务
    #本模块占用#0 I2S
  3. 将一定帧数的数据送入队列等待解码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import hzmp3

    inputData = open("test.mp3", "rb").read() #readbinary
    volumeFactor = "音量等级[0,5]" #0为原音量,其余为原音量/2^volumeFactor
    frameCount = "送入的数据帧数"
    bytesPerFrame = "每帧数据字节数"

    if hzmp3.playMp3(inputData, volumeFactor, frameCount, bytesPerFrame):
    print("Success!") #返回bool
    else:
    print("Fail!")

    #如果系统处于暂停状态,调用此方法会切换成播放状态
    #开发者需要确定第一帧和普通帧数据的长度
  4. 快进快退
    1
    2
    3
    4
    5
    6
    7
    import hzmp3

    timeS = "快进/快退秒数,复数为快退"
    frameSizeCommon = "音频文件普通帧字节数"
    frameSizeFirst = "第一帧字节数"

    hzmp3.progressSeek(timeS, frameSizeCommon, frameSizeFirst)
  5. 一些简单方法
    1
    2
    3
    4
    5
    6
    import hzmp3

    hzmp3.getTime() #获取已播放时间
    hzmp3.pauseOrContinue() #切换播放/暂停
    hzmp3.isFinish() #判断队列是否播放完毕
    hzmp3.mp3DeInit() #释放MP3解码器和I2S资源,并销毁另一个核心的播放任务

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import hzmp3
from machine import SoftSPI
from machine import Pin
from BN165DKBDriver import keyCode
import machine
import utime,time
#I2S
I2Sbck=14
I2Sws=38
I2Ssd=12

#将秒数转化为 分钟:秒数 格式字符串的方法
def MS(I):
#秒数除以60取整得到分钟数
M=int(I/60)
#秒数对60取模得到余数秒数
S=I%60
#结果字符串中的分钟子串
strM=str(M)
#结果字符串中的秒数子串
strS=str(S)
#若分钟数小于10则在分钟子串前面补0
if(M<10):
strM="0"+str(M)
#若秒数小于10则在秒数子串前面补0
if(S<10):
strS="0"+str(S)
return strM+":"+strS


#主程序开始
#设置当前CPU的工作频率为240MHz(默认160MHz)
machine.freq(240000000)
#无冲数字键盘对应的IO口
CP=Pin(41,Pin.OUT)
CE=Pin(39,Pin.OUT)
PL=Pin(40,Pin.OUT)
Q7=Pin(21,Pin.IN)
adcIn=(CP,CE,PL,Q7)

#要播放的MP3文件名称
mp3Name="xxdd.mp3"
#音量等级(可选0-5整数)
volume=0
#单次送入队列解码的数据帧数
frameNum=-1
#最大读取字节数,这里为8KB,大小要合适
maxReadBytes=1024*8
#打印信息
print("playing ",mp3Name)
#闪存中的MP3文件
mp3File = open(mp3Name,'rb')
#若文件不存在则打印提示信息
if(not mp3File):
print(f"filename:{mp3Name} not found!")

#分4次读取总数为2048字节(2KB)的数据,服务于获取待播放MP3的信息
tempMp3Data=mp3File.read(512)
tempMp3Data=tempMp3Data+mp3File.read(512)
tempMp3Data=tempMp3Data+mp3File.read(512)
tempMp3Data=tempMp3Data+mp3File.read(512)

#将文件指针跳转到文件末尾
mp3File.seek(0,2)
#获取文件总字节数
FileSize=mp3File.tell()
#获取待播放MP3的信息

mp3Info=hzmp3.getMp3Info(tempMp3Data,2048,FileSize)
#得到总时长
totalTime=mp3Info[0]
#得到第一帧的数据字节数
frameSizeFirst=mp3Info[1]
#得到普通帧的数据字节数
frameSizeCommon=mp3Info[2]
#计算出单次送入队列解码的数据帧数(以普通帧计)
frameNum=maxReadBytes//frameSizeCommon #int div
#得到采样率
samplerRate=mp3Info[3]
#得到通道数
channels=mp3Info[4]
#初始化MP3及I2S

hzmp3.initMp3(I2Sbck,I2Sws,I2Ssd,samplerRate,channels)
#将文件指针跳转到文件开始
mp3File.seek(0)
#是否为第一帧数据标志
isFirst=True
#待解码的一批数据(可以包含一帧或多帧数据),初始时为空
inputData=bytearray([])
#是否读取下一帧数据标志
readNext=True
#已播放时间打印时间戳
printTimeStamp=0

#播放循环
while True:
#获取按键编号
kc=keyCode(adcIn)
#本次需要读取的字节数=一帧数据长度*送入的帧数
needBytes=frameSizeCommon*frameNum
#一轮需要的数据如果超过512字节则需要分批读取
#获取当前的已播放时间(以秒计)
currTime=hzmp3.getTime()
#若距离上一次打印已播放时间信息已经超过了2秒(即2000ms)
if(utime.ticks_ms()-printTimeStamp>2000):
#更新时间戳
printTimeStamp=utime.ticks_ms()
#打印当前的已播放时间
print(MS(currTime))
#若按下键盘左侧右键
if(kc==3):
#执行快进,并获取快进后的文件字节索引
seek=hzmp3.progressSeek(5,frameSizeCommon,frameSizeFirst)
#将文件指针跳转到对应字节处
mp3File.seek(seek)
#设置需要读取下一帧数据
readNext=True
#打印当前的已播放时间
print(MS(currTime))
#休眠一段时间,防止按键连按过快
time.sleep(0.2)
#若按下键盘左侧左键
elif(kc==1):
#执行快退,并获取快退后的文件字节索引
seek=hzmp3.progressSeek(-5,frameSizeCommon,frameSizeFirst)
#如果快退到文件开始,则设置需要读取第一帧数据
if(seek==0):
isFirst=True
#将文件指针跳转到对应字节处
mp3File.seek(seek)
#设置需要读取下一帧数据
readNext=True
#打印当前的已播放时间
print(MS(currTime))
#休眠一段时间,防止按键连按过快
time.sleep(0.2)

#当前需要解码的一批帧数据的帧字节数
currFrameByteCount=frameSizeCommon
#如果需要解码的是第一帧
if(isFirst):
#设置为不需要读取第一帧数据
isFirst=False
#设置本次需要读取的字节数为frameSizeFirst
#否则保持needBytes为frameNum个普通帧所需的字节数
needBytes=frameSizeFirst
#当前需要解码的一批帧数据的帧字节数
currFrameByteCount=frameSizeFirst
#如果数据送入成功
if(readNext):
#清空当前数据,以备读取下一帧数据
inputData=bytearray([])
#分步读取needBytes字节的数据,每步最多读取512字节
while(needBytes>0):
if needBytes>512:
inputData=inputData+mp3File.read(512)
needBytes=needBytes-512
else:
inputData=inputData+mp3File.read(needBytes)
needBytes=0
#将当前待解码的一批数据送入解码模块解码并播放
# 一批帧数据 音量控制参数 帧数 每帧字节数
readNext=hzmp3.playMp3(inputData,volume,len(inputData)//currFrameByteCount,currFrameByteCount)
#若播放队列中没有数据且文件已经读取完毕
if(hzmp3.isFinish() and len(inputData)==0):
#打印结束信息
print("play finished.....")
break

#释放MP3播放相关资源
hzmp3.mp3DeInit()
#关闭当前歌曲文件
mp3File.close()