当前位置: 首页 > java >正文

《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 timesck_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 availablebits=16,         # 数据宽度16bit,2字节format=I2S.MONO, # 单通道MONO, 双通道STEREOrate=16000,      # 采样率16000Hzibuf=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 timesck_pin = Pin(14)
ws_pin = Pin(13)
sd_in_pin = Pin(12)
sd_out_pin = Pin(45)# sd引脚要设置为sd_out_pin
# 这里要注意用I2S.TXaudio_i2s = I2S(0, sck=sck_pin, ws=ws_pin, sd=sd_out_pin, mode=I2S.TX,     # only TX mode availablebits=16,         # 数据宽度16bit,2字节format=I2S.MONO, # 单通道MONO, 双通道STEREOrate=16000,      # 采样率16000Hzibuf=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+=1audio_i2s.write(buffer)else:breakaudio_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 IDheader.extend(struct.pack('<I', 36 + data_size))  # Chunk Sizeheader.extend(b'WAVE')  # Formatheader.extend(b'fmt ')  # Subchunk1 IDheader.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 Sizeheader.extend(b'data')  # Subchunk2 IDheader.extend(struct.pack('<I', data_size))  # Subchunk2 Sizereturn 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 = 0while 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文件头中的字段(如声道数、数据大小)必须与音频数据严格匹配,否则可能导致播放失败。

http://www.xdnf.cn/news/3164.html

相关文章:

  • 90.如何将Maui应用安装到手机(最简) C#例子 Maui例子
  • 西门子PLC S7-1200电动机软启动、软停止的控制实例
  • Android 移动开发:ProgressBar(转圈进度条)
  • 基于go的简单管理系统(增删改查)
  • Linux基础 -- Generic Netlink 框架详解与开发实践
  • UI设计之photoshop学习笔记
  • ⛺️ Sui Basecamp 2025 最新日程
  • C# 类的基本概念(从类的内部访问成员和从类的外部访问成员)
  • AXI总线设计高带宽or低带宽?你需要做个选择
  • 大规模克希霍夫积分法叠前深度偏移中,并行化和旅行时表处理
  • 11.模方ModelFun工具-指定置平
  • 【Docker】Docker拉取部分常用中间件
  • 音视频项目在微服务领域的趋势场景题深度解析
  • 为Mac用户定制的云服务器Vultr 保姆级教程
  • 运维打铁: 存储方案全解析
  • 《可信数据空间 技术架构》技术文件正式发布
  • 出现Invalid bound statement (not found)问题的原因可能有哪些
  • 分布式数字身份:迈向Web3.0世界的通行证 | 北京行活动预告
  • IoTDB集群部署中的网络、存储与负载配置优化
  • 研发效率破局之道阅读总结(4)个人效率
  • C#学习笔记 项目引用添加异常
  • C++继承(上)
  • 一、OrcaSlicer源码编译
  • VOIP的信令技术有哪些,区别是什么?
  • 【教学类-102-21】蝴蝶三色图作品3——异型书蝴蝶“满格变形图”一页2图、一页4图
  • ubuntu 部署moodle
  • Java Set<String>:如何高效判断是否包含指定字符串?
  • 私有知识库 Coco AI 实战(六):打造 ES Mapping 小助手
  • 你的项目有‘哇‘点吗?
  • LabelVision - yolo可视化标注工具