Skip to content
~/nihildigit
Go back

ESP32_软解mp3音频文件

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

T型板的不足

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

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

认识本课程所需的设备

  1. 基于ESP32-S3的多媒体开发平台

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

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

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

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

  1. 基于ESP32-S3的智能摄像头

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

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

配套软件系统的重要性

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

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

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

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

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

背景知识

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

hzmp3 模块包含的工具方法

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

  1. 获取MP3文件信息
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
  1. 初始化
import hzmp3

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

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

#初始化解码器,I2S和工作在另一个核心的播放任务
#本模块占用#0 I2S
  1. 将一定帧数的数据送入队列等待解码
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!")

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

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

hzmp3.progressSeek(timeS, frameSizeCommon, frameSizeFirst)
  1. 一些简单方法
import hzmp3

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

案例

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()

Share this note on:

Next note
How_Computers_Really_Work_01_计算概念