本节目标:分清 I2S 的三根信号线和三种时序模式,理解 PCM 与 PDM 的区别,能独立看懂 ESP-IDF 5.x 的「PDM 麦克风接收 + 标准 I2S 发送」代码,完成录音与回放实验
一、本节要解决的问题
前面几节打交道的都是「慢变量」:按键、温度、超声波,几百毫秒读一次完全够用。声音不行------要还原一段人声,每秒至少得采几千次,而且采样间隔必须均匀,差一个节拍就会失真。如果把这件事交给 CPU 用 ADC 一点点采,既要保证节拍,又要同时把数据喂给喇叭,很容易顾此失彼。
I2S 就是为音频设计的硬件串行总线:外设自己按位时钟搬数据,CPU 只需要准备一块内存。再配上数字麦克风和数字功放,整条链路可以做到没有一个模拟环节:
| 环节 | 模拟方案 | I2S 数字方案 |
|---|---|---|
| 麦克风 | 模拟麦克风 + ADC 连续采样 | PDM 数字麦克风,直接输出 1 bit 流 |
| 传输 | 模拟电压走线,容易串扰 | 三根数字线,抗干扰 |
| 功放 | DAC + 模拟功放 | 数字功放芯片内置 DAC |
| CPU 负担 | 定时器 + ADC 中断逐点采样 | DMA 搬完一块再处理 |
本节的两个例程是这条链路的两种落地方式:
audio_record:录 10 秒,存进内部 Flash 的 SPIFFS 分区(/spiffs/record.pcm),然后回放。audio_record_sdcard:录 10 秒,按 WAV 格式存进 SD 卡(/sdcard/record.wav),然后回放。WAV 带文件头,拔下卡插电脑就能直接播。
二、硬件基础
2.1 引脚连接
| 用途 | 信号 | GPIO | 对端 |
|---|---|---|---|
| I2S 发送 | BCLK | GPIO46 | 功放芯片位时钟 |
| I2S 发送 | LRCLK / WS | GPIO9 | 功放芯片声道选择 |
| I2S 发送 | SDOUT | GPIO8 | 功放芯片数据输入 |
| PDM 接收 | PDM_CLK | GPIO3 | 麦克风时钟 |
| PDM 接收 | PDM_DATA | GPIO42 | 麦克风数据 |
| IO 扩展 | I2C SDA / SCL | GPIO10 / GPIO11 | XL9555,地址 0x20 |
| SD 卡 | CS / MOSI / MISO / SCK | GPIO4 / 5 / 7 / 6 | TF 卡座(SPI 模式) |
几个容易忽略的点:
- 功放使能不在 SoC 上 。喇叭使能脚接在 XL9555 的
IO0_0上,必须先把 XL9555 配置成输出再拉高,功放才工作。代码里xl9555_ioconfig((~(SPK_EN_IO)) & 0xFFFF)把除IO0_0外的引脚全设为输入,只有IO0_0是输出。 - 没有用 MCLK 。
i2s_std_config_t里.mclk = I2S_GPIO_UNUSED;功放只需要 BCLK、WS、DATA 三根线,主时钟由芯片内部自己恢复。 - 麦克风只有两根线。PDM 麦克风不接收采样率参数,它只跟着 CLK 的节拍把 1 bit 数据推上 DATA;采样在麦克风内部完成。
- I2S0 和 I2S1 分工 。ESP32-S3 的 PDM 模式只有 I2S0 支持,所以麦克风固定接
I2S_NUM_0;喇叭用标准模式,接I2S_NUM_1。两个通道相互独立,可以同时工作。
三、知识点
3.1 三根信号线
| 信号 | 全称 | 作用 |
|---|---|---|
| BCLK | Bit Clock | 位时钟,每个边沿搬一位 |
| LRCLK | Left/Right Clock,也叫 WS | 声道选择,频率等于采样率 |
| SD | Serial Data | 数据线,MSB 先行 |
| MCLK | Master Clock | 主时钟,本实验不用 |
- 主机(Master)负责产生 BCLK 和 LRCLK,从机(Slave)跟着节拍收发。例程里 ESP32-S3 是主机,麦克风和功放都是从机。
- 一帧 = 一个采样周期(1/fs)。标准 I2S 的一帧固定包含左、右两个槽,单声道时只有一个槽有有效数据,另一个槽补 0。
- 帧内位数 = 位宽 × 槽数。例如 16 bit × 2 槽 = 每帧 32 个 BCLK。
3.2 位宽与位深
课件用一张 32 bit 的时序图区分这两个概念:
- 位宽(word length):传输时每个样本占用的时钟位数,属于接口设计参数,常见 16/24/32。
- 位深(bit depth):样本量化后的有效精度,属于数据本身的参数。
- 位宽大于位深时,多出来的低位补 0。例如位宽 32、位深 24:前 24 位是有效数据,后 8 位是 0。
注意:位深决定动态范围,位宽只决定传输时间。用 32 位宽传 16 位深的音频不会更清晰,只是每帧多花了一倍时钟。
3.3 三种时序模式
| 模式 | ESP-IDF 宏 | LRCLK 电平 | 数据延迟 | 对齐方式 |
|---|---|---|---|---|
| 标准(飞利浦) | I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG |
低=左,高=右 | 1 个 BCLK | MSB 比 LRCLK 跳变晚 1 个 BCLK |
| 左对齐 | I2S_STD_MSB_SLOT_DEFAULT_CONFIG |
高=左,低=右 | 无 | MSB 与 LRCLK 跳变沿对齐 |
| 右对齐 | I2S_STD_LSB_SLOT_DEFAULT_CONFIG |
高=左,低=右 | 无 | LSB 与 LRCLK 跳变沿对齐,高位补 0 |
补充说明:
- 三种模式都是「BCLK 下降沿发送、上升沿采样」,差别只在 LRCLK 极性和数据相对帧边界的位置。
- 发送方和接收方必须约定同一种模式,否则听到的是噪声或错位的声道。
- 本实验的功放芯片按标准飞利浦模式接收,所以代码用
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG。
3.4 采样率
- 采样率 = 每秒钟采多少个点,常见 8 kHz、16 kHz、44.1 kHz、44 kHz、48 kHz。
- LRCLK 的频率就是采样率:一帧对应一个采样点。
- 采样率越高越接近原声,数据量也线性增长。语音用 16k~24k 足够,音乐要 44.1k/48k。
- 奈奎斯特采样定理:采样率至少是信号最高频率的 2 倍。人声主要能量在 4 kHz 以下,8 kHz 采样就能听懂,24 kHz 更自然。
- 例程取 24 kHz:语音清晰度和文件大小折中,10 秒单声道 16 bit 约 480 KB。
3.5 PCM 脉冲编码调制
课件给的链路是:麦克风 → PCM 调制 → 主控 → DAC → 喇叭。PCM 分三个步骤:
- 采样:按固定时间间隔记录模拟电压。
- 量化:把每个样本的幅度近似到最近的量化级。
- 编码:把量化级写成二进制。
- 采样率决定时间分辨率,采样位数(位深)决定幅度分辨率。
- PCM 是未经压缩的原始音频,文件里只有样本值,没有格式说明。播放时必须知道采样率、位宽、声道数,否则速度或音调会不对。
- WAV 就是「44 字节文件头 + PCM 数据」:文件头把采样率、位宽、声道数写在最前面,播放器先读头再解码。
3.6 PDM 脉冲密度调制
- PDM 只用 1 bit 表示幅度,用「1 的密度」表达大小:幅度越大,1 越密;静音时输出连续的 0 或 1 的平衡点。
- 调制方法是 Δ-Σ(delta-sigma);解调时做低通/抽取滤波,把 1 bit 流还原成 PCM。
- 优点:麦克风里只需要一个 1 bit 调制器,成本低、抗干扰;缺点是数据率极高(时钟常在 1~3 MHz),必须靠硬件抽取。
- ESP32-S3 的 I2S 外设在 PDM 接收模式下内置抽取滤波器,配置好采样率后读出来就是 16 bit PCM,CPU 不参与解调。
- PDM 的 DATA 线上是左右声道交替的 1 bit 流;单声道麦克风靠 SEL 引脚决定自己占左槽还是右槽,代码用
slot_mask选择。
3.7 实验功能框图
课件最后一页给出的录音实验流程,用文字展开就是:
PDM 麦克风 --PDM_CLK/PDM_DATA--> ESP32-S3 I2S0(PDM RX) --PCM--> 写文件
|
SPIFFS(内部 Flash) 或 SD 卡(WAV)
|
喇叭 <--功放芯片<--I2S1(标准 TX)<--PCM 数据<--读文件
录音和回放是两个独立方向:录音走 I2S0 的 PDM 接收,回放走 I2S1 的标准发送。两个方向共用同一份 PCM 数据格式(16 bit、24 kHz、单声道),所以存下来的数据可以直接拿去播放。
四、代码分析
4.1 工程结构
audio_record/
├── CMakeLists.txt
├── partitions_audio.csv # 8 MB 的 audio(SPIFFS) 分区
├── sdkconfig # 目标 esp32s3,16 MB Flash,八线 PSRAM
└── main/
├── audio_record.c # app_main + 录音/回放流程
├── nocodec_audio.c/.h # I2S 初始化与读写封装
└── xl9555.c/.h # IO 扩展芯片驱动
audio_record_sdcard 的结构类似,多一个 format_wav.h(WAV 文件头结构体和宏),少一个 SPIFFS 分区表。
partitions_audio.csv 的内容:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
audio, data, spiffs, , 8M,
应用分区只有 1 MB,剩下 8 MB 全给 SPIFFS 存录音。sdkconfig 里 CONFIG_PARTITION_TABLE_CUSTOM=y 指定了这张表。
4.2 引脚定义与主流程
c
//I2S 主时钟
#define I2S_BCLK GPIO_NUM_46
//I2S 声道选择线
#define I2S_WS GPIO_NUM_9
//I2S 数据线
#define I2S_SDOUT GPIO_NUM_8
//功放芯片使能引脚对应XL9555的IO0_0
#define SPK_EN_IO IO0_0
//PDM麦克风的CLK
#define PDM_CLK GPIO_NUM_3
//PDM麦克风数据引脚
#define PDM_DATA GPIO_NUM_42
//采样率
#define SAMPLE_RATE 24000
app_main 的执行顺序是:配置 IO 扩展芯片 → 初始化喇叭 → 初始化麦克风 → 挂载文件系统 → 录音 10 秒 → 开功放 → 回放 → 关功放。
c
void app_main(void)
{
//XL9555初始化
xl9555_init(XL9555_SDA,XL9555_SCL,GPIO_NUM_NC,NULL);
//把除了IO0_0引脚外,都设置为输入
xl9555_ioconfig((~(SPK_EN_IO))&0xFFFF);
//初始化喇叭
init_speaker(I2S_BCLK,I2S_WS,I2S_SDOUT,24000);
//初始化PDM麦克风
init_pdm_microphone(PDM_DATA,PDM_CLK,24000);
//初始化spiff文件系统
audio_spiffs_init();
//开始录音10秒
start_record(10);
xl9555_pin_write(SPK_EN_IO,1);
vTaskDelay(pdMS_TO_TICKS(100));
//开始播放
start_play();
xl9555_pin_write(SPK_EN_IO,0);
while(1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
注意 :
init_speaker和init_pdm_microphone的第 4 个参数直接写了24000,而不是宏SAMPLE_RATE。改采样率时如果只改宏,这两处还是 24 kHz,录音和回放就会不一致。这是一个复制代码时留下的坑。
4.3 喇叭:标准 I2S 发送
c
void init_speaker(gpio_num_t bclk,gpio_num_t ws,gpio_num_t dout,uint32_t sample_rate)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
chan_cfg.auto_clear_after_cb = true;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
i2s_std_config_t i2s_rx_cfg ={
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_rate),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = bclk,
.ws = ws,
.dout = dout,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
i2s_rx_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &i2s_rx_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
}
逐项解释:
I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER):用通道 1、主机模式,其余参数(DMA 描述符数量、每帧字数)用默认值。auto_clear_after_cb = true:DMA 缓冲区下溢时自动填 0。回放结束或数据没跟上时,不会把旧数据反复送出去,避免「卡带」一样的杂音。i2s_new_channel(&chan_cfg, &tx_handle, NULL):第二个参数是发送句柄,第三个是接收句柄。只发送,接收传NULL。I2S_STD_CLK_DEFAULT_CONFIG(sample_rate):时钟源默认用 PLL_160M,mclk_multiple取 256,按采样率自动算分频。I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(16BIT, MONO):16 位、单声道、飞利浦标准时序。slot_mask = I2S_STD_SLOT_RIGHT:单声道时只把数据放在右槽,左槽补 0。.mclk = I2S_GPIO_UNUSED:功放不需要主时钟,省一根线。
代码痕迹 :这个函数里保存配置的变量叫
i2s_rx_cfg,其实是发送配置,是从接收例程复制过来的;不影响功能,但读代码时别被名字误导。
4.4 麦克风:PDM 接收
c
void init_pdm_microphone(gpio_num_t dat,gpio_num_t clk,uint32_t sample_rate)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(sample_rate),
/* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.clk = clk,
.din = dat,
.invert_flags = {
.clk_inv = false,
},
},
};
pdm_rx_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT;
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
- 通道固定
I2S_NUM_0:ESP32-S3 只有 I2S0 支持 PDM 模式。i2s_new_channel(&chan_cfg, NULL, &rx_handle)的第三参数是接收句柄。 I2S_PDM_RX_CLK_DEFAULT_CONFIG(sample_rate):PDM 时钟和抽取率都按采样率算好,改速度只改这一个入参。I2S_PDM_RX_SLOT_DEFAULT_CONFIG(16BIT, MONO):硬件抽取后直接输出 16 bit PCM。slot_mask = I2S_STD_SLOT_RIGHT:只取右槽。注意这里用的是标准模式的枚举I2S_STD_SLOT_RIGHT,而字段属于 PDM 配置;两者数值碰巧相同,能编过,但类型不对。
4.5 录音:PDM → PCM → SPIFFS
c
void start_record(uint32_t rec_time)
{
int flash_wr_size = 0;
//每次采样的数据长度(单位:字)
const size_t read_size_word = 8192;
//根据录音时间计算出需要录的长度(计算公式:采样率*录音时间 = 录音数据长度)
const int flash_rec_time = SAMPLE_RATE*rec_time;
FILE *f = fopen(AUDIO_MOUNT"/record.pcm", "w");
if (f == NULL)
{
ESP_LOGE(TAG, "Failed to open file for writing");
return;
}
int16_t* i2s_read_buff = (int16_t*)malloc(read_size_word*sizeof(int16_t));
ESP_LOGI(TAG,"Start record");
while (flash_wr_size < flash_rec_time)
{
int read_word = audio_read(i2s_read_buff,read_size_word);
if (read_word)
{
//声音放大,可以去掉,放大是为了听得更清楚
for(int i = 0;i<read_word;i++)
{
i2s_read_buff[i] = i2s_read_buff[i]<<1;
}
ESP_LOGI(TAG,"audio read word:%d",read_word);
fwrite(i2s_read_buff, read_word*2, 1, f);
flash_wr_size += read_word;
}
else
{
ESP_LOGI(TAG,"Read Failed!\n");
}
}
ESP_LOGI(TAG, "Recording done!");
fclose(f);
}
- 计数单位是字 (
int16_t,一个采样一个字),所以flash_rec_time = 24000 × 10 = 240000个字,写入时再乘 2 变成字节。 - 每次读 8192 个字(16 KB),10 秒大约循环 29 次。
<<1是把幅度翻倍,注释写明了只是为了听得更清楚,可以去掉。对这种简单左移要有警惕:超过int16_t范围会溢出翻转,形成爆音。- 文件里只有裸 PCM,没有文件头。总大小 240000 × 2 = 480000 字节。
4.6 回放:SPIFFS → I2S1 → 功放
c
void start_play(void)
{
const size_t write_size_byte = 8192;
struct stat st;
if(stat(AUDIO_MOUNT"/record.pcm",&st) == 0)
{
ESP_LOGI(TAG,"record.pcm filesize:%ld",st.st_size);
}
FILE *f = fopen(AUDIO_MOUNT"/record.pcm", "r");
if(!f)
{
ESP_LOGI(TAG,"record.pcm open fail!");
return;
}
ESP_LOGI(TAG,"Start play");
uint8_t *i2s_write_buff = malloc(write_size_byte);
if(!i2s_write_buff)
{
fclose(f);
return;
}
size_t read_byte = 0;
do
{
fread(i2s_write_buff,write_size_byte,1,f);
audio_write((const int16_t*)i2s_write_buff,write_size_byte/2);
read_byte += write_size_byte;
vTaskDelay(pdMS_TO_TICKS(10));
} while (read_byte < st.st_size);
free(i2s_write_buff);
fclose(f);
ESP_LOGI(TAG,"Play done");
}
- 每次从文件读 8192 字节,按 16 bit 拆成 4096 个字送进 I2S。
audio_write内部调用i2s_channel_write(..., 200),写完后返回实际写入的字数。- 每轮延时 10 ms,让 DMA 有机会把缓冲送到功放,避免喂得太快。
- 回放前
xl9555_pin_write(SPK_EN_IO, 1)打开功放,并延时 100 ms 等它上电稳定;回放结束后再拉低,省电也避免底噪。
4.7 SD 卡版:多一个 WAV 文件头
audio_record_sdcard 的录音流程和 SPIFFS 版基本一样,区别在存储和文件格式:
c
uint32_t flash_rec_time = BYTE_RATE * rec_time;
const wav_header_t wav_header =
WAV_HEADER_PCM_DEFAULT(flash_rec_time, BIT_SAMPLE, SAMPLE_RATE, 1);
...
FILE *f = fopen(SD_MOUNT_POINT"/record.wav", "a");
fwrite(&wav_header, sizeof(wav_header), 1, f);
WAV_HEADER_PCM_DEFAULT 把 WAV 的 44 字节头一次填好:
| 字段 | 值 | 含义 |
|---|---|---|
chunk_id |
RIFF |
文件标识 |
chunk_size |
数据长度 + 44 - 8 | 从本字段之后到文件尾的长度 |
chunk_format |
WAVE |
格式类型 |
subchunk_id |
fmt |
格式块 |
audio_format |
1 | PCM |
num_of_channels |
1 | 单声道 |
sample_rate |
24000 | 采样率 |
byte_rate |
48000 | 每秒字节数 = 24000 × 16 / 8 |
block_align |
2 | 每个采样字节数 |
bits_per_sample |
16 | 位深 |
data 块大小 |
480000 | 10 秒的 PCM 数据 |
其它差异:
- SD 卡走 SPI:
mount_sdcard()用SDSPI_HOST_DEFAULT(),把 CS/MOSI/MISO/SCK 接到 GPIO4/5/7/6,挂载点/sdcard。 - 这一版放大倍数更大:
i2s_readraw_buff[i] << 2,相当于 4 倍。 xl9555驱动的接口也换了一版:SPIFFS 版直接传 SDA/SCL,SD 卡版先i2c_new_master_bus()再把总线句柄传给xl9555_init()。两份驱动是各自复制的,不能混用。
五、关键 API 速查
| API 或宏 | 作用 | 头文件 |
|---|---|---|
i2s_new_channel |
申请 I2S 通道,指定收发方向 | driver/i2s_common.h |
i2s_channel_init_std_mode |
按标准 I2S 模式初始化 | driver/i2s_std.h |
i2s_channel_init_pdm_rx_mode |
按 PDM 接收模式初始化 | driver/i2s_pdm.h |
i2s_channel_enable / i2s_channel_disable |
启动 / 停止通道 | driver/i2s_common.h |
i2s_channel_read |
从 RX 通道读 PCM 数据 | driver/i2s_common.h |
i2s_channel_write |
向 TX 通道写 PCM 数据 | driver/i2s_common.h |
I2S_CHANNEL_DEFAULT_CONFIG |
通道默认配置(DMA 数量、帧数) | driver/i2s_common.h |
I2S_STD_CLK_DEFAULT_CONFIG |
标准模式时钟配置(采样率) | driver/i2s_std.h |
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG |
飞利浦时序、位宽、声道模式 | driver/i2s_std.h |
I2S_PDM_RX_CLK_DEFAULT_CONFIG |
PDM 接收时钟配置 | driver/i2s_pdm.h |
I2S_PDM_RX_SLOT_DEFAULT_CONFIG |
PDM 接收位宽、声道模式 | driver/i2s_pdm.h |
esp_vfs_spiffs_register |
挂载 SPIFFS 分区 | esp_spiffs.h |
esp_vfs_fat_sdspi_mount |
以 SPI 方式挂载 FAT 文件系统 | esp_vfs_fat.h |
xl9555_init / xl9555_ioconfig / xl9555_pin_write |
IO 扩展芯片初始化 / 方向 / 输出 | xl9555.h |
六、编译与现象验证
例程不在本工作区,先拉源码:
bash
git clone https://gitee.com/vi-iot/esp32s3-board.git
cd esp32s3-board/audio_record
仓库 README 注明依赖 ESP-IDF v5.3.3 或 v5.4.2 :代码用了 driver/i2s_std.h、driver/i2s_pdm.h 和新的 driver/i2c_master.h,4.x 编不过。编译:
bash
idf.py set-target esp32s3
idf.py build
idf.py -p COMx flash monitor
audio_record 用自定义分区表 partitions_audio.csv,sdkconfig 里已打开 CONFIG_PARTITION_TABLE_CUSTOM=y;audio_record_sdcard 用默认分区表。开发板需要是 16 MB Flash + PSRAM 的 ESP32-S3 板子,否则分区表放不下 8 MB 的 audio 分区。
6.1 audio_record 的预期日志
I (xxx) main: Start record
I (xxx) main: audio read word:8192
...
I (xxx) main: Recording done!
I (xxx) main: record.pcm filesize:480000
I (xxx) main: Start play
I (xxx) main: Play done
10 秒 × 24000 样本 = 240000 个 16 bit 样本,正好 480000 字节。audio read word:8192 会出现约 29 次(240000 / 8192 ≈ 29.3)。
6.2 audio_record_sdcard 的预期日志
I (xxx) pdm_rec_example: Initializing SD card
...
I (xxx) pdm_rec_example: Starting recording for 10 seconds!
I (xxx) pdm_rec_example: Recording done!
I (xxx) pdm_rec_example: Starting play wav
录完把 TF 卡拔下来插电脑,record.wav 应能直接用播放器打开。