小秦哥 发表于 2025-4-11 23:16:09

MicroPython 开辟ESP32应用教程 之 I2S、INMP441音频录制、MAX98357A音频播放、SD卡读写

本课程我们讲解Micropython for ESP32 的i2s及其应用,比如INMP441音频录制、MAX98357A音频播放等,还有SD卡的读写。
一、硬件预备

1、支持micropython的ESP32S3开辟板
2、INMP441数字全向麦克风模块
3、MAX98357A音频播放模块
4、SD卡模块
5、面包板及连接线若干
连接方式:
https://i-blog.csdnimg.cn/direct/b79b22ca4d0a48b8806429be478fb8ac.png                 https://i-blog.csdnimg.cn/direct/cc855d5fe6574572b4c8498699db75a1.png
inmp441MAX98357AESP32S3SDIO13WSIO12SCKIO11L/R接地SD接VCCGAIN接地DINIO37BCLKIO38LRCIO39 SD卡模块ESP32S3SCKIO4MOSIIO5MISOIO16CSIO17        
二、i2s介绍 


一)、I2S协议基础

I2S(Inter-IC Sound)是一种同步串行通讯协议,专为数字音频设备设计,支持单向/双向音频数据传输。其物理层包含三条信号线:


[*]‌SCK‌(串行时钟):同步数据传输速率
[*]‌WS‌(字选择):区分左右声道或定义采样率
[*]‌SD‌(串行数据):传输现实音频数据流‌
二)、MicroPython I2S类特性

        A.  仅支持主设备利用模式,可控制SCK和WS信号的天生,适用于连接麦克风、
             DAC等从设备‌
        B. 支持ESP32、STM32、RP2等主流微控制器平台,通过统一接口简化跨硬件开辟‌
三)、核心功能实现


[*] ‌音频输入/输出‌

[*]‌灌音‌:从麦克风模块获取PCM音频数据
[*]‌播放‌:向DAC或音频解码器发送音频流‌27。

[*] ‌参数机动设置‌
初始化时可设置关键参数:
i2s = I2S(id,# 硬件实例编号(如I2S.NUM0)
          sck=Pin(11), ws=Pin(12), sd=Pin(13),# 引脚映射
          mode=I2S.RX,# 模式(RX/TX)
          bits=16,      # 采样位深
          format=I2S.MONO,# 声道格式 MONO为单声道,STEREO为立体声
          rate=16000,   # 采样率
          ibuf=8092)   # 输入缓冲区大小‌:ml-citation{ref="4,7" data="citationList"}

[*] ‌中断与DMA支持‌
支持异步数据读写,通过DMA减少CPU占用率,提拔实时性‌
 
 四)、范例应用场景


[*] ‌音频播放器‌
播放WAV/MP3文件(需解码库支持)‌。
[*] ‌语音采集系统‌
连接INMP441等数字麦克风实现情况音录制‌。
[*] ‌实时语音处置惩罚‌
联合神经网络进行关键词辨认或声纹分析‌
三、MicroPython SD卡介绍 

一)、SD卡初始化与挂载

硬件接口设置‌
使用SPI模式连接SD卡(需4线:CLK/MOSI/MISO/CS),范例ESP32设置示例:
from sdcard import SDCard
import os, time, gc

spi = SPI(2,
                  baudrate=80000000,
                  polarity=0,
                  phase=0,
                  sck=Pin(4),
                  mosi=Pin(5),
                  miso=Pin(16))
sd = SDCard(spi,Pin(17,Pin.OUT))
二)、文件利用API

        基础文件读写‌
        使用标准文件利用接口:
 
def test_sd():
    os.mount(sd,'/sd')
    # 重新查询系统文件目录
    print('挂载SD后的系统目录:{}'.format(os.listdir()))
    with open("/sd/test.txt", "w") as f:
            f.write(str("Hello MicroPython!"))

    # 从sd卡目录下读取hello.txt文件内容
    with open("/sd/test.txt", "r") as f:
      # 打印读取的内容
      data = f.read()
      print (data)
四、inmp4411录制音频

通过前面的讲解,这一末节的内容需要掌握的知识点我们都已经掌握,直接上代码:
audiofilename = '/sd/rec.pcm'
def record_audio(filename=audiofilename, duration=5, sample_rate=16000):
#   # 硬件诊断
    print("初始化I2S...")
    try:
      i2s = I2S(
            0,
            sck=Pin(11), ws=Pin(12), sd=Pin(13),
            mode=I2S.RX,
            bits=16,
            format=I2S.MONO,
            rate=sample_rate,
            ibuf=4096
      )
    except Exception as e:
      print("I2S初始化失败:", e)
      return

    # 计算数据量
    bytes_per_second = sample_rate * 2# 16bit=2字节
    total_bytes = bytes_per_second * duration
#    header = createWavHeader(sample_rate, 16, 1, total_bytes)
   
    # 录音循环
    try:
      with open(audiofilename, 'wb') as f:
#            f.write(header)
            start_time = time.ticks_ms()
            bytes_written = 0
            buffer = bytearray(2048)# 小缓冲区减少内存压力
            
            while bytes_written < total_bytes:
                read = i2s.readinto(buffer)
                if read == 0:
                  print("警告:未读取到数据")
                  continue
               
                f.write(buffer[:read])
                bytes_written += read
                gc.collect()
               
                # 实时进度
                elapsed = time.ticks_diff(time.ticks_ms(), start_time) / 1000
                print(f"进度: {bytes_written/total_bytes*100:.1f}%, 时间: {elapsed:.1f}s")
               
    except OSError as e:
      print("文件写入错误:", e)
    finally:
      i2s.deinit()
#      print("录音结束,文件大小:", os.stat(audiofilename), "字节")
      print("录音结束,文件大小:", bytes_written, "字节") 但这里需要分析一下的是,我们刚开始开辟的时候,录制的音频文件中的数据满是0,也就是说没有声音,噪音都没有,检查连接线、换IO口等等,各种折腾,但题目依然存在,后来因为出了别的的错误,就暂停了,具体可以参考:MicroPython 开辟ESP32应用教程 之 WIFI、BLE共用常见题目处置惩罚及中断处置惩罚函数留意事项
上文中提到的题目处置惩罚完后,我们继续折腾音频录制及播放的功能,奇怪的事情发生了,连接好各功能模块后,测试,居然好了,怀疑是上文中提到的电源的题目,但把外接电源移除,测试没有题目。
也就是说,到现在,我们照旧不知道之前为什么有题目?现在为什么好了?只能怀疑电源不稳?
五、MAX98357A音频播放

这个也没什么好讲,直接上代码吧
audiofilename = '/sd/rec.pcm'

audio_out = I2S(1, sck=Pin(38), ws=Pin(39), sd=Pin(37), mode=I2S.TX, bits=16, format=I2S.MONO, rate=16000, ibuf=20000)
def play_audio(filename='/sd/rec.wav', duration=5, sample_rate=16000):      

#    audio_out.volume(80)
    with open(audiofilename,'rb') as f:

      # 跳过文件的开头的44个字节,直到数据段的第1个字节
#      pos = f.seek(44)
   
      # 用于减少while循环中堆分配的内存视图
      wav_samples = bytearray(1024)
      wav_samples_mv = memoryview(wav_samples)
         
      print("开始播放音频...")
      
      #并将其写入I2S DAC
      while True:
            try:
                num_read = f.readinto(wav_samples_mv)
               
                # WAV文件结束
                if num_read == 0:
                  break
   
                # 直到所有样本都写入I2S外围设备
                num_written = 0
                while num_written < num_read:
                  num_written += audio_out.write(wav_samples_mv)
                  
            except Exception as ret:
                print("产生异常...", ret)

六、完整代码
 

该代码简单修改可生存为WAV格式文件,可以用我们常见的音频播放软件播放。
from machine import I2S, Pin,SPI
from sdcard import SDCard
import os, time, gc

spi = SPI(2,
                  baudrate=20000000,
                  polarity=0,
                  phase=0,
                  sck=Pin(4),
                  mosi=Pin(5),
                  miso=Pin(16))
sd = SDCard(spi,Pin(17,Pin.OUT))

audiofilename = '/sd/rec.pcm'
def createWavHeader(sampleRate, bitsPerSample, num_channels, datasize):   
    riff_size = datasize + 36 - 8# 修正RIFF块大小
    header = bytes("RIFF", 'ascii')
    header += riff_size.to_bytes(4, 'little')
    header += bytes("WAVE", 'ascii')
    header += bytes("fmt ", 'ascii')
    header += (16).to_bytes(4, 'little')          # fmt块大小
    header += (1).to_bytes(2, 'little')            # PCM格式
    header += num_channels.to_bytes(2, 'little')   # 声道数
    header += sampleRate.to_bytes(4, 'little')   # 采样率
    header += (sampleRate * num_channels * bitsPerSample // 8).to_bytes(4, 'little')# 字节率
    header += (num_channels * bitsPerSample // 8).to_bytes(2, 'little')# 块对齐
    header += bitsPerSample.to_bytes(2, 'little')# 位深
    header += bytes("data", 'ascii')
    header += datasize.to_bytes(4, 'little')       # 数据块大小
    return header

def record_audio(filename=audiofilename, duration=5, sample_rate=16000):
#   # 硬件诊断
    print("初始化I2S...")
    try:
      i2s = I2S(
            0,
            sck=Pin(11), ws=Pin(12), sd=Pin(13),
            mode=I2S.RX,
            bits=16,
            format=I2S.MONO,
            rate=sample_rate,
            ibuf=4096
      )
    except Exception as e:
      print("I2S初始化失败:", e)
      return

    # 计算数据量
    bytes_per_second = sample_rate * 2# 16bit=2字节
    total_bytes = bytes_per_second * duration
#    header = createWavHeader(sample_rate, 16, 1, total_bytes)
   
    # 录音循环
    try:
      with open(audiofilename, 'wb') as f:
#            f.write(header)
            start_time = time.ticks_ms()
            bytes_written = 0
            buffer = bytearray(1024)# 小缓冲区减少内存压力
            
            while bytes_written < total_bytes:
                read = i2s.readinto(buffer)
                if read == 0:
                  print("警告:未读取到数据")
                  continue
               
                f.write(buffer[:read])
                bytes_written += read
                gc.collect()
               
                # 实时进度
                elapsed = time.ticks_diff(time.ticks_ms(), start_time) / 1000
                print(f"进度: {bytes_written/total_bytes*100:.1f}%, 时间: {elapsed:.1f}s")
               
    except OSError as e:
      print("文件写入错误:", e)
    finally:
      i2s.deinit()
#      print("录音结束,文件大小:", os.stat(audiofilename), "字节")
      print("录音结束,文件大小:", bytes_written, "字节")
      
audio_out = I2S(1, sck=Pin(38), ws=Pin(39), sd=Pin(37), mode=I2S.TX, bits=16, format=I2S.MONO, rate=16000, ibuf=20000)
def play_audio(filename='/sd/rec.wav', duration=5, sample_rate=16000):      

#    audio_out.volume(80)
    with open(audiofilename,'rb') as f:

      # 跳过文件的开头的44个字节,直到数据段的第1个字节
#      pos = f.seek(44)
   
      # 用于减少while循环中堆分配的内存视图
      wav_samples = bytearray(1024)
      wav_samples_mv = memoryview(wav_samples)
         
      print("开始播放音频...")
      
      #并将其写入I2S DAC
      while True:
            try:
                num_read = f.readinto(wav_samples_mv)
               
                # WAV文件结束
                if num_read == 0:
                  break
   
                # 直到所有样本都写入I2S外围设备
                num_written = 0
                while num_written < num_read:
                  num_written += audio_out.write(wav_samples_mv)
                  
            except Exception as ret:
                print("产生异常...", ret)



if __name__ == "__main__":
    try:
      os.mount(sd,'/sd')   
      record_audio(duration=5)
      play_audio()
    except Exception as e:
      print("异常:",e)
# 测试

'''

import time
from machine import I2S, Pin
import math

# I2S配置
i2s = I2S(0,
          sck=Pin(22), ws=Pin(23), sd=Pin(21),
          mode=I2S.RX,
          bits=16,
          rate=16000,
          channel_format=I2S.ONLY_LEFT)

# 参数配置
SILENCE_THRESHOLD = 0.02# 需根据环境噪声校准
CHECK_INTERVAL = 0.1      # 检测间隔(秒)
SILENCE_DURATION = 1.0    # 目标静默时长

buffer = bytearray(1024)# 512个16位样本
last_sound_time = time.time()

while True:
    i2s.readinto(buffer)# 读取I2S数据‌:ml-citation{ref="6" data="citationList"}
   
    # 计算当前块RMS值
    sum_sq = 0
    for i in range(0, len(buffer), 2):
      sample = int.from_bytes(buffer, 'little', True)
      sum_sq += (sample / 32768) ** 2# 16位有符号转浮点‌:ml-citation{ref="6" data="citationList"}
    rms = math.sqrt(sum_sq / 512)
   
    # 更新最后有声时间戳
    if rms > SILENCE_THRESHOLD:
      last_sound_time = time.time()
   
    # 判断静默持续时间
    if (time.time() - last_sound_time) >= SILENCE_DURATION:
      print("检测到持续静默")
      # 触发后续处理
'''


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: MicroPython 开辟ESP32应用教程 之 I2S、INMP441音频录制、MAX98357A音频播放、SD卡读写