ESP32 I2S音频总线学习笔记(八):添加按键控制功能 - 详解
简介
上篇文章我们实现了录音播放器的基本功能,实现了录音和播放,但这种情况下无法对我们的录音音频文件进行控制。本篇我们将给这个录音播放器添加按键控制功能,实现基本的录音控制和播放控制。之前我们录音的时候时间是30s,那如果我们添加按键控制录音的话,这个时间可以去掉,只通过按键去控制录音的时长,这里我想实现的功能是添加四个按键,按键1可以控制录音/暂停/继续,按键2是结束录音,按键3是播放上一条录音,按键4是播放下一条。
主要硬件
ESP32主控:

INMP441全向麦克风模块:
PCM5102A 立体声DAC模块 :
SD卡模块:
硬件接线
ESP32和麦克风INMP441:
| ESP32 | INMP441 |
|---|---|
| D13 | SCK |
| D12 | WS |
| D14 | SD |
| 3.3V | VDD |
| GND | GND |
ESP32和PCM5102A:
| ESP32 | PCM5102A |
|---|---|
| - | VCC |
| 3.3V | 3.3V |
| GND | GND |
| GND | FLT、DMP、SCL (这里SCL悬空可能会有干扰,所以接地) |
| D27 | BCK |
| D25 | DIN |
| D26 | LCK |
| GND | FMT |
| 3.3V | XMT |
ESP32和SD模块接线:
| ESP32 | SD模块 |
|---|---|
| D5 | CS |
| D18 | SCK |
| D23 | MOSI |
| D19 | MISO |
| 5V | VCC |
| GND | GND |
ESP32和按键接线
| ESP32 | 按键 |
|---|---|
| 32 | 按键1 录音/暂停/继续 |
| 33 | 按键2 结束录音 |
| 22 | 按键3 播放上一条录音 |
| 21 | 按键4 播放下一条录音 |
按键的另一个引脚接地,低电平触发。
实现思路
这里简单说一下实现思路,在前面的基础上我们已经实现将麦克风采集到的录音数据写入SD卡了,所以我们只需要对音频逻辑部分进行处理即可,因为我们录音的时候需要判断当前状态是正在录音还是结束录音,所以我们可以定义一个状态变量recordState去判断,然后当按键按下的时候进行录音相关控制(暂停/继续)或者播放相关控制(上一条/下一条)。不同于之前只录音一个文件,实现多次录音就需要给每个文件进行命名,如audio0, audio1, audio2(假设命名从零开始),而且我们要保证录音保存进去SD卡成功后,系统复位或重新上电,也能从上一次录音文件的命名序号继续递增命名,可以定义一个全局变量recordCount来保存当前录音文件数,创建一个txt文档(如果不存在)将这个值写入。每次上电或复位后,通过扫描 SD 卡上的音频文件,确定最大编号并重新读取这个recordCount的值进行更新,这样就解决了文件命名的问题。实际测试发现使用delay函数进行按键的抖动延时效果不佳,因此下面介绍下OneButton库的使用。
OneButton库
OneButton是一个用于Arduino的开源库,旨在简化单个按钮的多功能输入处理。通过这个库,你可以轻松地检测按钮的单击、双击和长按等事件,从而为你的项目增加交互性。OneButton库不仅提高了代码的可读性和可维护性,还减少了硬件投资,因为你可以在同一个按钮上实现多种功能。
这个库的使用方法及源代码链接如下:https://github.com/mathertel/OneButton
要使用这个库,需要在Arduino IDE中先引入OneButton库,在“工具”菜单中,选择“库管理器”,然后搜索“OneButton”,选择“OneButton”库并安装。

要使用OneButton库,我们需要创建一个OneButton对象。对象需要三个参数:按钮引脚、按钮类型(HIGH或LOW)和按钮模式(上拉或者下拉)。
// Declare and initialize
OneButton btn = OneButton(
BUTTON_PIN, // Input pin for the button
true, // Button is active LOW
true // Enable internal pull-up resistor
);
要响应按钮事件,我们需要定义回调函数。回调函数是一个函数,当按钮被单击、双击或长按时被调用。
以下代码定义了一个回调函数,当按钮被单击时,将向串口输出一条消息:
static void handleClick() {
Serial.println("Clicked!");
// digitalWrite(2,LED_STATE);
}
要绑定回调函数,我们需要使用OneButton.attachClick()、OneButton.attachDoubleClick()和OneButton.attachLongPress()函数。这些函数需要一个参数:回调函数的名称。
例如,以下代码将handleClick()函数绑定到OneButton对象的单击事件上:
button.attachClick(handleClick);
这里比较重要又容易忘记的是,为了处理按钮事件,我们需要在loop()函数中调用OneButton.tick()函数。该函数检测按钮事件并调用相应的回调函数。
以下代码在loop()函数中调用OneButton.tick()函数:
void loop() {
btn.tick();
}
下面是一个完整的OneButton代码示例。该代码将按钮连接到引脚35,当按钮被单击时,向串口输出“Clicked!”消息(也可加上点灯功能)
#include <Arduino.h>
#include <OneButton.h>
#define BUTTON_PIN 35
#define LED_STATE HIGH
// Declare and initialize
OneButton btn = OneButton(
BUTTON_PIN, // Input pin for the button
true, // Button is active LOW
true // Enable internal pull-up resistor
);
static void handleClick() {
Serial.println("Clicked!");
//digitalWrite(2,LED_STATE);
}
// Single Click event attachment
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
btn.attachClick(handleClick);
pinMode(2, OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
btn.tick();
}
结合我们要实现的功能即,按键1控制录音/暂停/继续,按键2结束录音,按键3上一条录音,按键4下一条录音,创建四个OneButton对象:
// 按钮引脚定义
#define RECORD_PAUSE_BUTTON 32 // 按钮1:录音/暂停/继续
#define STOP_BUTTON 33 // 按钮2:结束录音
#define PLAY_PRE_BUTTON 35 // 按钮3:播放最近录音
#define PLAY_NEXT_BUTTON 21 // 按钮3:播放最近录音
OneButton btn1 = OneButton(
RECORD_PAUSE_BUTTON, // Input pin for the button
true, // Button is active LOW
true // Enable internal pull-up resistor
);
OneButton btn2 = OneButton(
STOP_BUTTON, // Input pin for the button
true, // Button is active LOW
true // Enable internal pull-up resistor
);
OneButton btn3 = OneButton(
PLAY_PRE_BUTTON, // Input pin for the button
true, // Button is active LOW
true // Enable internal pull-up resistor
);
OneButton btn4 = OneButton(
PLAY_NEXT_BUTTON, // Input pin for the button
true, // Button is active LOW
true // Enable internal pull-up resistor
);
定义了四个回调函数,当按钮被单击时,执行录音,结束,播放功能。这里我们先搭建整体按钮控制的整体框架,声明函数,后面再完善细节。
static void RECORD_PAUSE_BUTTON_Click();
static void STOP_BUTTON_Click();
static void PLAY_PRE_BUTTON_Click();
static void PLAY_NEXT_BUTTON_Click();
同时将其这四个回调函数分别绑定到OneButton对象的单击事件上:
void setup(){
btn1.attachClick(RECORD_PAUSE_BUTTON_Click); // 按钮1:录音/暂停/继续
btn2.attachClick(STOP_BUTTON_Click); // 按钮2:结束录音
btn3.attachClick(PLAY_PRE_BUTTON_Click); //按钮3:播放上条录音
btn4.attachClick(PLAY_NEXT_BUTTON_Click); //按钮4:播放下条录音
}
最重要别忘记在loop()函数中调用OneButton.tick()函数:
void loop(){
btn1.tick();
btn2.tick();
btn3.tick();
btn4.tick();
}
回调函数
关于回调函数的音频逻辑处理部分前面提到过,可以定义一个状态变量recordState去判断,这里可以使用枚举类型:
enum RecordState { STOPPED,
RECORDING,
PAUSED };
RecordState recordState = STOPPED; // 录音状态
录音及暂停按钮的回调函数:
static void RECORD_PAUSE_BUTTON_Click() {
if (recordState == STOPPED) {
if (isPlaying) { // 录音时停止播放
isPlaying = false;
if (playFile) playFile.close();
i2s_zero_dma_buffer(I2S_NUM_1);
}
// 开始录音
recordState = RECORDING;
String fileName = "/audio" + String(recordCount) + ".wav";
recordFile = SD.open(fileName, FILE_WRITE);
if (recordFile) {
WavHeader header;
header.dataSize = 0;
header.chunkSize = sizeof(WavHeader) - 8;
recordFile.write((uint8_t*)&header, sizeof(header));
total_samples = 0;
Serial.println("开始录音: " + fileName);
} else {
Serial.println("无法创建文件: " + fileName);
recordState = STOPPED;
}
} else if (recordState == RECORDING) {
// 暂停录音
recordState = PAUSED;
i2s_stop(I2S_NUM_1);
Serial.println("录音暂停");
delay(500);
} else if (recordState == PAUSED) {
// 继续录音
recordState = RECORDING;
i2s_start(I2S_NUM_1);
Serial.println("继续录音");
delay(500);
}
}
停止录音回调函数:
static void STOP_BUTTON_Click() {
if (recordState == RECORDING || recordState == PAUSED) {
recordState = STOPPED;
if (recordFile) {
// 更新 WAV 头
WavHeader header;
header.dataSize = total_samples * sizeof(int16_t);
header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;
recordFile.seek(0);
recordFile.write((uint8_t*)&header, sizeof(header));
recordFile.close();
Serial.println("录音结束并保存: /audio" + String(recordCount) + ".wav");
recordCount++;
saveRecordCount(); // 保存 recordCount
}
i2s_zero_dma_buffer(I2S_NUM_1);
}
}
播放上条录音回调函数:
static void PLAY_PRE_BUTTON_Click() {
if (recordCount > 0) {
if (curFileNum > 0) {
curFileNum--; // 上一首
} else {
curFileNum = recordCount - 1; // 跳到最后一个文件
}
String fileName = "/audio" + String(curFileNum) + ".wav";
Serial.println("播放上一首: " + fileName);
startPlay(fileName);
} else {
Serial.println("没有可播放的录音文件");
}
}
播放下条录音回调函数:
static void PLAY_NEXT_BUTTON_Click() {
if (recordCount > 0) {
if (curFileNum < recordCount - 1) {
curFileNum++; // 下一首
} else {
curFileNum = 0; // 跳到第一个文件
}
String fileName = "/audio" + String(curFileNum) + ".wav";
Serial.println("播放下一首: " + fileName);
startPlay(fileName);
} else {
Serial.println("没有可播放的录音文件");
}
}
功能实现
到这一步按钮部分的功能我们已经基本完善,关于I2S初始化输入,输出以及WAV文件定义的部分,前面很多篇文章都有所提及,这里就不再详细说明了。
还有一步是扫描 SD 卡上的音频文件,以及保存 recordCount 到配置文件中,这里的recordCount在每次上电复位后会根据文件动态更新。
// 读取 recordCount 从配置文件
void loadRecordCount() {
File configFile = SD.open("/record_count.txt", FILE_READ);
if (configFile) {
String countStr = configFile.readStringUntil('\n');
recordCount = countStr.toInt();
configFile.close();
Serial.println("从配置文件读取 recordCount: " + String(recordCount));
} else {
Serial.println("无法读取 record_count.txt,扫描文件...");
recordCount = 0;
}
// 扫描 SD 卡上的音频文件,确定最大编号
int maxIndex = -1;
for (int i = 0; i < 1000; i++) { // 假设最多1000个文件
String fileName = "/audio" + String(i) + ".wav";
if (!SD.exists(fileName)) {
break;
}
maxIndex = i;
}
if (maxIndex + 1 > recordCount) {
recordCount = maxIndex + 1;
Serial.println("根据文件扫描更新 recordCount: " + String(recordCount));
}
}
// 保存 recordCount 到配置文件
void saveRecordCount() {
SD.remove("/record_count.txt"); // 删除旧文件
File configFile = SD.open("/record_count.txt", FILE_WRITE);
if (configFile) {
configFile.println(recordCount);
configFile.close();
Serial.println("保存 recordCount: " + String(recordCount));
} else {
Serial.println("无法保存 record_count.txt");
}
}

总结
本篇文章在上一篇文章的基础上,对录音播放器增加了按键控制功能,包括录音的控制、暂停、及录音文件的播放选择,并且介绍了OneButton库的简单使用,并将它用于我们的录音播放器上。由于完整代码较长,感兴趣的可以在评论区留言获取完整代码!
浙公网安备 33010602011771号