前言
在智能硬件和物聯網應用中,音頻處理能力正成為越來越重要的功能——無論是語音交互、環境音采集,還是音樂播放,都離不開高效的音頻數據傳輸與處理。而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 格式的文件。 |
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 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文件頭中的字段(如聲道數、數據大小)必須與音頻數據嚴格匹配,否則可能導致播放失敗。