esp32cam micropython使用I2S驱动DAC模块播放音频

研究了一下,发现esp32cam的两个DAC引脚被摄像头占用了,分别是25和26。我们只能用I2S总线输出数字音频,之后用MAX98357模块将数字信号转成模拟信号。下面是代码:

MAX98357

导入模块和定义引脚:

from machine import I2S
from machine import Pin
import time

# 初始化引脚定义
sck_pin = Pin(14)  # 串行时钟输出
ws_pin = Pin(13)   # 字时钟
sd_pin = Pin(12)   # 串行数据输出

# 初始化i2s
audio_out = I2S(1,sck=sck_pin,
            ws=ws_pin,
            sd=sd_pin,
            mode=I2S.TX,
            bits=16,
            format=I2S.MONO,
            rate=16000,
            ibuf=20000)

# 下面是官方文档,Google翻译的,不准确,可以自己去翻看
'''
sck 是串行时钟线的引脚对象

ws 是单词选择行的引脚对象

sd 是串行数据线的引脚对象

mode 指定接收或发送

bits 指定样本大小(位),16 或 32

format 指定通道格式,STEREO(左右声道) 或 MONO(单声道)

rate 指定音频采样率(样本/秒)

ibuf 指定内部缓冲区长度(字节)
'''

引脚接线:

ESP32 CAM  --- MAX98357

GPIO14 --- BCLK

GPIO13 -- LRC

GPIO12 -- DIN

GND -- GND

5V -- VCC

(建议焊接,插接的方式会导致杂音或者无法播放)

代码部分(建议使用第二种,减少内存开销):

方法一:

wavtempfile = "temp.wav"
wav = open(wavtempfile,'rb')

print('播放音频')
# 播放开始时间
start_time = time.ticks_us()
# 读取音频文件的二进制数据
buf = wav.read()
# wav文件的头部数据,不是实际的音频数据,是文件信息,所以我们要丢弃这部分数据
bufhead = 44
# 实际音频数据大小,等于总大小减去头部信息的大小
bufsize = len(buf) - bufhead
# 缓冲区大小,前面初始化的时候设置的最后一个参数
bufcap = 20000
# 音频总时长 us 文件大小(单位:B)/(采样率16000 x 每个采样点大小2 ÷ 秒换算成微秒1000000)
all_time = bufsize / 0.032
# 写入DAC的次数
bunum = 1
# 要写入的数据 开始位置
bufstart = bufhead
# 循环读取
while bufsize:
    # 读取结束位置,等于读取次数*缓冲区大小
    bufend = bufcap * bunum
    # 当结束位置大于总数据长度的时候,结束位置等于数据最后一位
    if bufend > len(buf):
        bufend = len(buf)
    # 要写入的数据
    bufwrite = buf[bufstart:bufend]
    # 写入数据
    num_written = audio_out.write(bufwrite)
    # 总大小减去每次写入的大小
    bufsize -= len(bufwrite)
    # 重新设置读取位置,为上次结束后一位
    bufstart = bufend
    # 读取次数
    bunum = bunum + 1

方法二:

wavtempfile = "temp.wav"
wav = open(wavtempfile,'rb')

# 前进到WAV文件中数据段的第一个字节
pos = wav.seek(44) 

#分配样本数组
#用于减少while循环中堆分配的内存视图
wav_samples = bytearray(1024)
wav_samples_mv = memoryview(wav_samples)

print('播放音频')
# 音频总时长 us 文件大小(单位:B)/(采样率16000 x 每个采样点大小2 ÷ 秒换算成微秒1000000)
all_time = (len(wav)-44) / 0.032
#从WAV文件中连续读取音频样本
#并将其写入I2S DAC
while True:
    try:
        num_read = wav.readinto(wav_samples_mv)
        num_written = 0
        #WAV文件结束
        if num_read == 0: 
            break
            #前进到数据段的第一个字节
            # pos = wav.seek(44) 
        # 在首次执行的时候记录开始时间
        if num_written == 0:
            # 记录播放开始时间
            start_time = time.ticks_us()
        #循环,直到所有样本都写入I2S外围设备
        while num_written < num_read:
            num_written += audio_out.write(wav_samples_mv[num_written:num_read])
            
    except Exception as e:
        print('错误,',e)
        break

最后关闭文件,注销i2s

# 等待音频播放完毕
while 1:
    # 播放结束时间
    end_time = time.ticks_us()
    # 如果当前时间减去开始播放的时间大于音频时长
    if (end_time - start_time) > all_time:
        # 取消初始化 I2S 总线
        audio_out.deinit()
        # 停止等待
        break
# 播放完毕
# 关闭文件
wav.close()

完成后:

esp32cam使用i2s驱动max98357播放音频

如果使用SD卡,必看

21.09.08补充:

以下补充针对esp32cam,esp32引脚足够,自己设置SD卡引脚后DAC输出就行,不用加max98357模块

因为esp32cam的内存太小,我尝试把音频文件放到SD卡内读取,结果会出现读取时扬声器电流声和仅读取一次后就无法再次使用SD卡。这是因为GPIO12、13、14、15在挂载SD卡的时候被占用了,这几个引脚刚好又被用作扬声器,所以在挂载和读取的时候会有电流声。

因为内存卡我在上电之后就马上挂载了,在后面的代码中才实例化I2S。所以这几个引脚一开始被SD占用,后来被扬声器占用,就会出现无法再次读取的情况。

import machine

print('挂载sd卡...')
try:
    os.mount(machine.SDCard(), "/sd")
    print('挂载成功,路径为:"/sd"')
    # 卸载
    # os.umount("/sd")
except Exception as e:
    print(e)

# I2S

from machine import I2S
sck_pin = Pin(14) 
ws_pin = Pin(13)  
sd_pin = Pin(12) 

audio_out = I2S(
            1,
            sck=sck_pin, 
            ws=ws_pin, 
            sd=sd_pin,
            mode=I2S.TX,
            bits=16,
            format=I2S.MONO,
            rate=16000,
            ibuf=20000)

我们可以将代码设置成

挂载SD卡->读取音频文件到内存->注销挂载SD卡->实例化I2S->播放音频->注销I2S->挂载SD卡

这样就可以反复读取内存卡了。不过依然会存在电流声,所以我选择了另一种方案:实时传输音频。

import io
import urequests

# 音频文件
wavname = 'test.wav'
# 请求音频文件
wavbuf = urequests.get('http://www.xxx.com/music/%s' % wavname).content
# 数据存到内存
wav = io.BytesIO(wavbuf)
# 以打开文件的方式读取内存数据
buf = wav.read()

因为在我的代码中socket已经被占用,所以我用的urequests下载wav文件来播放(如果socket没被占用,则可以用socket分片传输,用多线程边下边播,完美解决文件下载延迟问题)。解决电流声的同时,因为服务器和单片机在一个局域网,所以加载也没有延迟,完美解决~

版权声明:本文为CSDN博主「qq_33130395」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33130395/article/details/120117741

生成海报
点赞 0

qq_33130395

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

开发板接口及按键详解,全双工对讲模块

LoRa语音模块提供配套的开发板,含语音模块、Demo软 件、演示软件一套。 开发板包含四个控制按键,其中三个按键 PTT 键、UP 键和 DOWN 按键和模块的对应端口直接连接,开机后可以实现全部对应

PySerial库的简单用法

import serial API:pySerial API — pySerial 3.4 documentation 这次尝试适用PySerial库是为了从树莓派小车连接的UWB定位模块的串口中,获取与解析出串