《ESP32音频开发实战:I2S协议解析与WAV音频录制/播放全指南》

前言

在智能硬件和物联网应用中,音频处理能力正成为越来越重要的功能——无论是语音交互、环境音采集,还是音乐播放,都离不开高效的音频数据传输与处理。而I2S(Inter-IC Sound)作为专为音频设计的通信协议,正是实现这些功能的核心技术。

本文将以ESP32为例,深入剖析I2S协议的工作原理,详解TDM与PDM两种通信模式的差异,并通过实战代码演示如何用MicroPython实现音频录制(PCM原始数据)、WAV文件解析与播放。无论你是想打造一个语音识别设备、自定义音频播放器,还是探索实时音效处理,这篇指南都将为你提供从理论到实践的完整路径。

I2S简介

I2S(Inter-IC Sound,集成电路内置音频总线)是一种同步串行通信协议,通常用于在两个数字音频设备之间传输音频数据。

ESP32-S3 包含 2 个 I2S 外设。通过配置这些外设,可以借助 I2S 驱动来输入和输出采样数据。

TDM 通信模式(标准)

I2S 总线包含以下几条线路:

  • MCLK:主时钟线。该信号线可选,具体取决于从机,主要用于向 I2S 从机提供参考时钟。
  • BCLK:位时钟线。用于数据线的位时钟。
  • WS:字(声道)选择线。通常用于识别声道。
  • DIN/DOUT:串行数据输入/输出线。如果 DIN 和 DOUT 被配置到相同的 GPIO,数据将在内部回环。

PDM 通信模式

I2S 总线包含以下几条线路:

  • CLK:PDM 时钟线。
  • DIN/DOUT:串行数据输入/输出线。

每个 I2S 控制器都具备以下功能,可由 I2S 驱动进行配置:

  • 可用作系统主机或从机
  • 可用作发射器或接收器
  • DMA 控制器支持流数据采样,CPU 无需单独复制每个采样数据

每个控制器都有独立的 RX 和 TX 通道,连接到不同 GPIO 管脚,能够在不同的时钟和声道配置下工作。注意,尽管在一个控制器上 TX 通道和 RX 通道的内部 MCLK 相互独立,但输出的 MCLK 信号只能连接到一个通道。如果需要两个互相独立的 MCLK 输出,必须将其分配到不同的 I2S 控制器上。

. 对比总结

特性

TDM

PDM

核心目标

多路信号时分复用

高精度模数信号转换

适用场景

周期性数据(语音、固定速率流)

高动态模拟信号(音频、传感器)

抗噪能力

依赖信道质量

强(数字脉冲抗干扰)

硬件复杂度

中等(需同步电路)

低(单比特量化)

延迟

低(固定时隙)

较高(过采样+滤波)

参考链接: I2S - ESP32-S3 - — ESP-IDF 编程指南 v5.4.1 文档

为什么要学习I2S

  • 高质量音频传输:I2S是专为音频设计的通信协议,能够传输高质量的音频数据,适合音频播放、录音等应用。
  • 低延迟:I2S支持实时音频处理,适合对延迟要求高的场景,如语音识别或实时音频效果处理。
  • ESP32内置I2S外设:ESP32集成了I2S接口,可直接连接麦克风、DAC、ADC等音频设备,简化硬件设计。
  • 灵活性:I2S支持多种数据格式和采样率,适应不同的音频需求。
  • 音频播放与录音:可用于音乐播放器、录音设备等。
  • 语音识别与控制:适合智能音箱、语音助手等需要音频输入输出的设备。
  • 音效处理:支持实时音效处理,如均衡器、混音器等。
  • 低功耗:ESP32的I2S外设在低功耗模式下仍能高效工作,适合电池供电设备。
  • 高性能:ESP32的高性能处理器结合I2S,能够处理复杂的音频任务。

总之I2S有助于开发高质量的音频应用,扩展项目功能,尤其在物联网和智能设备领域具有广泛应用。丰富的资源和强大的硬件支持使得学习和开发更加便捷。

PCM原始数据

I2S录制声音

"""
使用I2S读取数据
数据宽度16bit
采样率16000Hz
缓冲区大小1024
"""

from machine import I2S
from machine import Pin
import time

sck_pin = Pin(14)
ws_pin = Pin(13)
sd_in_pin = Pin(12)
sd_out_pin = Pin(45)

audio_in = I2S(0, sck=sck_pin, ws=ws_pin, sd=sd_in_pin, 
               mode=I2S.RX,     # only RX mode available
               bits=16,         # 数据宽度16bit,2字节
               format=I2S.MONO, # 单通道MONO, 双通道STEREO
               rate=16000,      # 采样率16000Hz
               ibuf=2048        # 缓冲区大小1024字节
)
print("I2S init complete!")

# 等待I2S初始化完成
# time.sleep_ms(500)

# 所有数据的列表
frames = []

print("开始录制...")
# 录制5s
start = time.time()
# 读取数据
while True:
    if time.time() - start > 5:
        break
    # 创建一个字节数组
    buf = bytearray(1024)
    num = audio_in.readinto(buf)
    frames.append(buf)
    
# 将音频数据写到文件
with open("audio.pcm", "wb") as f:
    for frame in frames:
        f.write(frame)
        
audio_in.deinit();

print("录音结束:", len(frames), "帧")
# 合并所有数据
data = b''.join(frames)
print("数据长度:", len(data))

I2S播放声音

"""
使用I2S播放数据
数据宽度16bit
采样率16000Hz
缓冲区大小1024
"""

from machine import I2S
from machine import Pin
import time

sck_pin = Pin(14)
ws_pin = Pin(13)
sd_in_pin = Pin(12)
sd_out_pin = Pin(45)

# sd引脚要设置为sd_out_pin
# 这里要注意用I2S.TX

audio_i2s = I2S(0, sck=sck_pin, ws=ws_pin, sd=sd_out_pin, 
               mode=I2S.TX,     # only TX mode available
               bits=16,         # 数据宽度16bit,2字节
               format=I2S.MONO, # 单通道MONO, 双通道STEREO
               rate=16000,      # 采样率16000Hz
               ibuf=2048        # 缓冲区大小1024字节
)
print("I2S init complete!")

# 等待I2S初始化完成
#time.sleep_ms(500)
# 读取音频文件
print("playing...")
counter = 0
with open("./audio.pcm", "rb") as f:
    while True:
        buffer = f.read(1024)
        if buffer:
            print("counter: ", counter)
            counter+=1
            audio_i2s.write(buffer)
        else:
            break

audio_i2s.deinit()
print("play complete...")

WAV音频

WAV 文件的前 44 个字节是文件头部分,包含了音频文件的元数据(如采样率、位宽、声道数等)。WAV 文件头遵循 RIFF 格式规范,以下是其详细结构:


WAV 文件头结构(44 字节)

偏移量

字段名称

大小(字节)

描述

0

Chunk ID

4

固定为 "RIFF"

,表示文件是一个 RIFF 格式的文件。

4

Chunk Size

4

文件总大小减去 8 字节(即文件大小 - 8)。

8

Format

4

固定为 "WAVE"

,表示这是一个 WAV 文件。

12

Subchunk1 ID

4

固定为 "fmt "

,表示接下来的部分是格式信息。

16

Subchunk1 Size

4

格式信息的大小(通常是 16 字节)。

20

Audio Format

2

音频格式(PCM 为 1,表示未压缩)。

22

Num Channels

2

声道数(1 表示单声道,2 表示立体声)。

24

Sample Rate

4

采样率(如 44100 Hz)。

28

Byte Rate

4

每秒的字节数(Sample Rate * Num Channels * BitsPerSample / 8

)。

32

Block Align

2

每个采样点的字节数(Num Channels * BitsPerSample / 8

)。

34

Bits Per Sample

2

每个采样点的位数(如 16 位)。

36

Subchunk2 ID

4

固定为 "data"

,表示接下来的部分是音频数据。

40

Subchunk2 Size

4

音频数据的大小(字节数)。

44

Data

N

音频数据(从第 44 字节开始)。

解析wav格式数据

struct.unpack 是 Python 中用于将二进制数据解析为 Python 数据类型的函数。它通常用于处理二进制文件、网络协议数据或硬件设备的原始数据。struct.unpackstruct.pack 的逆操作,后者用于将 Python 数据类型打包为二进制数据。


struct.unpack 的基本用法
struct.unpack(fmt, buffer)
  • fmt:格式化字符串,指定如何解析二进制数据。
  • buffer:包含二进制数据的字节对象(如 bytesbytearray)。
  • 返回值: 返回一个元组,包含解析后的数据。

格式化字符串 (fmt)

格式化字符串由以下部分组成:

  1. 字节顺序(可选):
    • @:本地字节顺序(默认)。
    • =:本地字节顺序,忽略对齐。
    • <:小端序(低位字节在前)。
    • >:大端序(高位字节在前)。
    • !:网络字节顺序(大端序)。
  1. 数据类型
    • c:字符(1 字节)。
    • b:有符号字节(1 字节)。
    • B:无符号字节(1 字节)。
    • ?:布尔值(1 字节)。
    • h:有符号短整型(2 字节)。
    • H:无符号短整型(2 字节)。
    • i:有符号整型(4 字节)。
    • I:无符号整型(4 字节)。
    • l:有符号长整型(4 字节)。
    • L:无符号长整型(4 字节)。
    • q:有符号长长整型(8 字节)。
    • Q:无符号长长整型(8 字节)。
    • f:浮点型(4 字节)。
    • d:双精度浮点型(8 字节)。
    • s:字符串(需要指定长度,如 10s 表示 10 字节的字符串)。
    • p:Pascal 字符串(1 字节长度 + 字符串)。
    • x:填充字节(跳过 1 字节)。

示例 1:解析单个值
import struct

# 二进制数据(4 字节的无符号整型)
buffer = b'\x01\x00\x00\x00'

# 解析为无符号整型
value = struct.unpack('<I', buffer)
print(value)  # 输出: (1,)
示例 2:解析多个值
import struct

# 二进制数据(2 个有符号短整型)
buffer = b'\x01\x00\x02\x00'

# 解析为 2 个有符号短整型
values = struct.unpack('<2h', buffer)
print(values)  # 输出: (1, 2)
示例 3:解析混合类型
import struct

# 二进制数据(1 个无符号短整型 + 1 个浮点型)
buffer = b'\x01\x00\x00\x00\x00\x00\x80\x3f'

# 解析为无符号短整型和浮点型
values = struct.unpack('<Hf', buffer)
print(values)  # 输出: (1, 1.0)
示例 4:解析字符串
import struct

# 二进制数据(10 字节的字符串)
buffer = b'hello\x00\x00\x00\x00\x00'

# 解析为 10 字节的字符串
value = struct.unpack('<10s', buffer)
print(value)  # 输出: (b'hello\x00\x00\x00\x00\x00',)
示例 5:解析 WAV 文件头
import struct

# 假设这是 WAV 文件的前 44 字节
wav_header = b'RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00\x44\xAC\x00\x00\x10\xB1\x02\x00\x04\x00\x10\x00data\x00\x00\x00\x00'

# 解析 WAV 文件头
chunk_id = struct.unpack('<4s', wav_header[0:4])[0]
chunk_size = struct.unpack('<I', wav_header[4:8])[0]
format = struct.unpack('<4s', wav_header[8:12])[0]
subchunk1_id = struct.unpack('<4s', wav_header[12:16])[0]
subchunk1_size = struct.unpack('<I', wav_header[16:20])[0]
audio_format = struct.unpack('<H', wav_header[20:22])[0]
num_channels = struct.unpack('<H', wav_header[22:24])[0]
sample_rate = struct.unpack('<I', wav_header[24:28])[0]
bits_per_sample = struct.unpack('<H', wav_header[34:36])[0]

print("Chunk ID:", chunk_id)
print("Chunk Size:", chunk_size)
print("Format:", format)
print("Subchunk1 ID:", subchunk1_id)
print("Subchunk1 Size:", subchunk1_size)
print("Audio Format:", audio_format)
print("Num Channels:", num_channels)
print("Sample Rate:", sample_rate)
print("Bits Per Sample:", bits_per_sample)

注意事项
  1. 字节顺序
    • 确保格式化字符串中的字节顺序与数据的实际存储顺序一致。
    • 小端序(<)和大端序(>)是最常用的两种字节顺序。
  1. 数据对齐
    • 某些平台可能要求数据对齐,可以使用 @= 来指定本地字节顺序。
  1. 缓冲区大小
    • 确保缓冲区的大小与格式化字符串的要求一致,否则会抛出 struct.error
  1. 返回值
    • struct.unpack 始终返回一个元组,即使只解析一个值。

总结
  • struct.unpack 是 Python 中处理二进制数据的强大工具。
  • 通过格式化字符串,可以灵活地解析各种数据类型。
  • 在处理文件、网络协议或硬件数据时,struct.unpack 非常有用。

实操演练
from machine import I2S, Pin
import struct

# 配置I2S
i2s = I2S(
    0,  # I2S编号
    sck=Pin(14),  # 时钟引脚
    ws=Pin(13),   # 字选择引脚
    sd=Pin(45),   # 数据引脚
    mode=I2S.TX,  # 发送模式
    bits=16,      # 数据位宽
    format=I2S.MONO,  # 单声道
    rate=16000,   # 采样率
    ibuf=40000    # 输入缓冲区大小
)

# 解析WAV文件头
def parse_wav_header(file):
    header = file.read(44)  # WAV文件头长度为44字节
    if header[0:4] != b'RIFF' or header[8:12] != b'WAVE':
        raise ValueError("不是有效的WAV文件")
    ret = struct.unpack("4s",header[0:4])
    print("ret=",ret,header[0:4].decode())
    # 提取采样率、位宽、声道数等信息
    sample_rate = struct.unpack('<I', header[24:28])[0]
    bits_per_sample = struct.unpack('<H', header[34:36])[0]
    num_channels = struct.unpack('<H', header[22:24])[0]
    data_size = struct.unpack('<I', header[40:44])[0]
    
    return sample_rate, bits_per_sample, num_channels, data_size

# 打开WAV文件
with open('audio.wav', 'rb') as f:
    sample_rate, bits_per_sample, num_channels, data_size = parse_wav_header(f)
    
    # 播放音频数据
    buffer_size = 1024  # 每次读取的缓冲区大小
    while True:
        buffer = f.read(buffer_size)
        if not buffer:
            break  # 文件读取完毕
        i2s.write(buffer)  # 通过I2S发送音频数据

# 关闭I2S
i2s.deinit()

print("播放完成")

保存wav格式数据

from machine import I2S, Pin
import struct

# 配置I2S
i2s = I2S(
    0,  # I2S编号
    sck=Pin(14),  # 时钟引脚
    ws=Pin(13),   # 字选择引脚
    sd=Pin(12),   # 数据引脚
    mode=I2S.RX,  # 接收模式
    bits=16,      # 数据位宽
    format=I2S.MONO,  # 单声道
    rate=16000,   # 采样率
    ibuf=40000    # 输入缓冲区大小
)

# WAV文件参数
sample_rate = 16000  # 采样率
bits_per_sample = 16  # 位宽
num_channels = 1  # 单声道
duration = 5  # 录制时长(秒)
buffer_size = 1024  # 每次读取的缓冲区大小

# 计算总数据量
total_samples = sample_rate * duration
total_data_size = total_samples * num_channels * (bits_per_sample // 8)

# 创建WAV文件头
def create_wav_header(sample_rate, bits_per_sample, num_channels, data_size):
    # WAV文件头格式
    header = bytearray()
    header.extend(b'RIFF')  # Chunk ID
    header.extend(struct.pack('<I', 36 + data_size))  # Chunk Size
    header.extend(b'WAVE')  # Format
    header.extend(b'fmt ')  # Subchunk1 ID
    header.extend(struct.pack('<IHHIIHH', 
                              16, 
                              1, 
                              num_channels,
                              sample_rate,
                              sample_rate * num_channels * (bits_per_sample // 8),
                              num_channels * (bits_per_sample // 8),
                              bits_per_sample))  # Subchunk1 Size
    header.extend(b'data')  # Subchunk2 ID
    header.extend(struct.pack('<I', data_size))  # Subchunk2 Size
    return header

# 创建WAV文件头
wav_header = create_wav_header(sample_rate, bits_per_sample, num_channels, total_data_size)

# 打开文件并写入WAV文件头
with open('audio.wav', 'wb') as f:
    f.write(wav_header)

    # 读取音频数据并写入文件
    samples_read = 0
    while samples_read < total_samples:
        buffer = bytearray(buffer_size)
        i2s.readinto(buffer)  # 从I2S读取数据
        f.write(buffer)  # 写入文件
        samples_read += buffer_size // (bits_per_sample // 8)

# 关闭I2S
i2s.deinit()

print("录音完成,文件已保存为 audio.wav")

结语

通过本文的学习,你已经掌握了ESP32的I2S音频开发全流程:从硬件接口配置、PCM原始数据采集,到WAV文件头的解析与生成,最终实现完整的音频录制与播放功能。这些技术可以广泛应用于智能音箱、录音笔、实时语音传输等场景。

技术的价值在于创造。不妨尝试将这些代码扩展为更复杂的应用——比如结合Wi-Fi实现远程音频流传输,或添加回声消除算法提升音质。如果在实践中遇到问题,不妨回顾I2S的时序特性或WAV文件格式的细节,往往能从中找到答案。

声音是人与机器最自然的交互方式,而你现在已经握住了开启这扇大门的钥匙。愿你的项目因音频而生动,因技术而卓越! 🎵


小提示

  • 实际开发时,注意根据硬件(如麦克风、DAC模块)调整I2S的采样率、位宽等参数。

  • WAV文件头中的字段(如声道数、数据大小)必须与音频数据严格匹配,否则可能导致播放失败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛慕昭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值