前言
在智能硬件和物联网应用中,音频处理能力正成为越来越重要的功能——无论是语音交互、环境音采集,还是音乐播放,都离不开高效的音频数据传输与处理。而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 格式的文件。 |
4 | Chunk Size | 4 | 文件总大小减去 8 字节(即文件大小 - 8)。 |
8 | Format | 4 | 固定为 ,表示这是一个 WAV 文件。 |
12 | Subchunk1 ID | 4 | 固定为 ,表示接下来的部分是格式信息。 |
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 | 每秒的字节数( )。 |
32 | Block Align | 2 | 每个采样点的字节数( )。 |
34 | Bits Per Sample | 2 | 每个采样点的位数(如 16 位)。 |
36 | Subchunk2 ID | 4 | 固定为 ,表示接下来的部分是音频数据。 |
40 | Subchunk2 Size | 4 | 音频数据的大小(字节数)。 |
44 | Data | N | 音频数据(从第 44 字节开始)。 |
解析wav格式数据
struct.unpack
是 Python 中用于将二进制数据解析为 Python 数据类型的函数。它通常用于处理二进制文件、网络协议数据或硬件设备的原始数据。struct.unpack
是 struct.pack
的逆操作,后者用于将 Python 数据类型打包为二进制数据。
struct.unpack
的基本用法
struct.unpack(fmt, buffer)
fmt
:格式化字符串,指定如何解析二进制数据。buffer
:包含二进制数据的字节对象(如bytes
或bytearray
)。- 返回值: 返回一个元组,包含解析后的数据。
格式化字符串 (fmt
)
格式化字符串由以下部分组成:
- 字节顺序(可选):
-
@
:本地字节顺序(默认)。=
:本地字节顺序,忽略对齐。<
:小端序(低位字节在前)。>
:大端序(高位字节在前)。!
:网络字节顺序(大端序)。
- 数据类型:
-
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)
注意事项
- 字节顺序:
-
- 确保格式化字符串中的字节顺序与数据的实际存储顺序一致。
- 小端序(
<
)和大端序(>
)是最常用的两种字节顺序。
- 数据对齐:
-
- 某些平台可能要求数据对齐,可以使用
@
或=
来指定本地字节顺序。
- 某些平台可能要求数据对齐,可以使用
- 缓冲区大小:
-
- 确保缓冲区的大小与格式化字符串的要求一致,否则会抛出
struct.error
。
- 确保缓冲区的大小与格式化字符串的要求一致,否则会抛出
- 返回值:
-
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文件头中的字段(如声道数、数据大小)必须与音频数据严格匹配,否则可能导致播放失败。