在PlatformIO上编写ESP32 + INMP441 + MAX98357语音录制回放程序

72 阅读7分钟

3. platformio.ini配置

; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

; 启用PSRAM(如果板子支持)
board_build.arduino.memory_type = qio_opi

; 增加闪存分区大小(可选)
board_build.partitions = default_16MB.csv

; 编译优化
build_flags = 
    -DBOARD_HAS_PSRAM
    -mfix-esp32-psram-cache-issue
    ; 如果需要更多内存,可以禁用某些功能
    ; -DCONFIG_ARDUINO_ISR_IRAM=0

; 库依赖
lib_deps = 
    ; 如果需要高级音频处理,可以添加这些库
    ; https://github.com/pschatzmann/arduino-audio-tools
    ; pschatzmann/ESP32-A2DP@^1.9.3
    
; 串口监视器配置
monitor_filters = 
    time
    colorize
#include <Arduino.h>
#include "driver/i2s.h"

// 硬件引脚定义
#define I2S_WS_PIN      25  // Word Select (LRCLK)
#define I2S_BCK_PIN     26  // Bit Clock (BCLK)
#define I2S_DIN_PIN     33  // INMP441数据输出
#define I2S_DOUT_PIN    22  // MAX98357数据输入
#define BUTTON_PIN      27  // 控制按键
#define LED_PIN         2   // 状态指示灯

// I2S参数配置
#define SAMPLE_RATE     16000     // 采样率
#define SAMPLE_BITS     16        // 采样位数
#define BUFFER_LEN      1024      // 缓冲区长度
#define RECORD_SECONDS  5         // 录制时长(秒)
#define MAX_SAMPLES     (SAMPLE_RATE * RECORD_SECONDS)

// 全局变量
int16_t *audio_buffer = nullptr;
volatile bool is_recording = false;
volatile bool is_playing = false;
volatile bool button_pressed = false;
size_t total_samples_recorded = 0;

// I2S初始化函数
void setupI2S() {
    Serial.println("正在初始化I2S接口...");
    
    // I2S输入配置(INMP441)
    i2s_config_t i2s_config_in = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
        .sample_rate = SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 8,
        .dma_buf_len = BUFFER_LEN,
        .use_apll = false,
        .tx_desc_auto_clear = false,
        .fixed_mclk = 0
    };

    // I2S输出配置(MAX98357)
    i2s_config_t i2s_config_out = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
        .sample_rate = SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 8,
        .dma_buf_len = BUFFER_LEN,
        .use_apll = false,
        .tx_desc_auto_clear = false,
        .fixed_mclk = 0
    };

    // 引脚配置
    i2s_pin_config_t pin_config_in = {
        .bck_io_num = I2S_BCK_PIN,
        .ws_io_num = I2S_WS_PIN,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_DIN_PIN
    };

    i2s_pin_config_t pin_config_out = {
        .bck_io_num = I2S_BCK_PIN,
        .ws_io_num = I2S_WS_PIN,
        .data_out_num = I2S_DOUT_PIN,
        .data_in_num = I2S_PIN_NO_CHANGE
    };

    // 安装并启动I2S驱动
    esp_err_t err;
    
    err = i2s_driver_install(I2S_NUM_0, &i2s_config_in, 0, NULL);
    if (err != ESP_OK) {
        Serial.printf("I2S输入驱动安装失败: %s\n", esp_err_to_name(err));
        return;
    }
    i2s_set_pin(I2S_NUM_0, &pin_config_in);
    
    err = i2s_driver_install(I2S_NUM_1, &i2s_config_out, 0, NULL);
    if (err != ESP_OK) {
        Serial.printf("I2S输出驱动安装失败: %s\n", esp_err_to_name(err));
        return;
    }
    i2s_set_pin(I2S_NUM_1, &pin_config_out);
    
    Serial.println("I2S初始化完成");
}

// 按键中断服务程序
void IRAM_ATTR buttonISR() {
    button_pressed = true;
}

// 录制音频
void recordAudio() {
    if (audio_buffer != nullptr) {
        free(audio_buffer);
        audio_buffer = nullptr;
    }
    
    // 分配内存(使用PSRAM如果可用)
    #ifdef BOARD_HAS_PSRAM
        audio_buffer = (int16_t*)ps_malloc(MAX_SAMPLES * sizeof(int16_t));
        Serial.println("使用PSRAM分配音频缓冲区");
    #else
        audio_buffer = (int16_t*)malloc(MAX_SAMPLES * sizeof(int16_t));
        Serial.println("使用内部RAM分配音频缓冲区");
    #endif
    
    if (audio_buffer == nullptr) {
        Serial.println("内存分配失败!");
        return;
    }
    
    Serial.println("开始录制...");
    Serial.printf("录制时长: %d秒\n", RECORD_SECONDS);
    
    is_recording = true;
    digitalWrite(LED_PIN, HIGH);
    
    size_t bytes_read = 0;
    int16_t temp_buffer[BUFFER_LEN];
    total_samples_recorded = 0;
    
    unsigned long start_time = millis();
    
    while (millis() - start_time < RECORD_SECONDS * 1000) {
        // 从I2S读取数据
        i2s_read(I2S_NUM_0, temp_buffer, BUFFER_LEN * sizeof(int16_t), 
                &bytes_read, portMAX_DELAY);
        
        // 复制到主缓冲区
        size_t samples_read = bytes_read / sizeof(int16_t);
        if (total_samples_recorded + samples_read <= MAX_SAMPLES) {
            memcpy(audio_buffer + total_samples_recorded, 
                  temp_buffer, bytes_read);
            total_samples_recorded += samples_read;
        }
        
        // 显示进度
        static int last_percent = -1;
        int percent = (millis() - start_time) * 100 / (RECORD_SECONDS * 1000);
        if (percent != last_percent && percent % 10 == 0) {
            Serial.printf("录制进度: %d%%\n", percent);
            last_percent = percent;
        }
    }
    
    is_recording = false;
    digitalWrite(LED_PIN, LOW);
    
    Serial.println("录制完成!");
    Serial.printf("录制了 %d 个样本 (%d KB)\n", 
                  total_samples_recorded, 
                  total_samples_recorded * sizeof(int16_t) / 1024);
}

// 播放音频
void playAudio() {
    if (audio_buffer == nullptr || total_samples_recorded == 0) {
        Serial.println("没有音频数据可播放!");
        return;
    }
    
    Serial.println("开始播放...");
    
    is_playing = true;
    digitalWrite(LED_PIN, HIGH);
    
    size_t samples_played = 0;
    size_t bytes_written = 0;
    
    while (samples_played < total_samples_recorded) {
        size_t remaining_samples = total_samples_recorded - samples_played;
        size_t samples_to_write = min((size_t)BUFFER_LEN, remaining_samples);
        
        i2s_write(I2S_NUM_1, 
                  audio_buffer + samples_played,
                  samples_to_write * sizeof(int16_t),
                  &bytes_written, 
                  portMAX_DELAY);
        
        samples_played += bytes_written / sizeof(int16_t);
        
        // 显示进度
        static int last_percent = -1;
        int percent = samples_played * 100 / total_samples_recorded;
        if (percent != last_percent && percent % 20 == 0) {
            Serial.printf("播放进度: %d%%\n", percent);
            last_percent = percent;
        }
    }
    
    is_playing = false;
    digitalWrite(LED_PIN, LOW);
    
    Serial.println("播放完成!");
}

// 显示系统信息
void showSystemInfo() {
    Serial.println("\n=== ESP32语音录制回放系统 ===");
    Serial.printf("采样率: %d Hz\n", SAMPLE_RATE);
    Serial.printf("采样位数: %d 位\n", SAMPLE_BITS);
    Serial.printf("最大录制时长: %d 秒\n", RECORD_SECONDS);
    Serial.printf("最大样本数: %d\n", MAX_SAMPLES);
    Serial.printf("缓冲区大小: %d 样本\n", BUFFER_LEN);
    
    // 内存信息
    Serial.printf("可用堆内存: %d KB\n", ESP.getFreeHeap() / 1024);
    #ifdef BOARD_HAS_PSRAM
        Serial.printf("PSRAM大小: %d KB\n", ESP.getPsramSize() / 1024);
        Serial.printf("可用PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
    #endif
    
    Serial.println("使用方法:");
    Serial.println("1. 短按按键: 开始/停止录制");
    Serial.println("2. 长按按键(>3秒): 播放录制内容");
    Serial.println("3. 双按按键: 清除录制内容");
    Serial.println("=============================\n");
}

// Arduino初始化函数
void setup() {
    Serial.begin(115200);
    delay(1000);  // 等待串口初始化
    
    Serial.println("系统启动中...");
    
    // 初始化GPIO
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);
    
    // 设置按键中断(下降沿触发)
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
    
    // 初始化I2S
    setupI2S();
    
    // 显示系统信息
    showSystemInfo();
    
    Serial.println("系统就绪,等待按键操作...");
}

// Arduino主循环
void loop() {
    static unsigned long last_button_time = 0;
    static int button_press_count = 0;
    static unsigned long button_down_time = 0;
    static bool button_was_down = false;
    
    // 检查按键状态(非中断方式)
    bool button_state = digitalRead(BUTTON_PIN) == LOW;
    
    if (button_state && !button_was_down) {
        // 按键按下
        button_down_time = millis();
        button_was_down = true;
    } 
    else if (!button_state && button_was_down) {
        // 按键释放
        unsigned long press_duration = millis() - button_down_time;
        
        if (press_duration < 3000) {
            // 短按
            button_press_count++;
            
            if (millis() - last_button_time > 500) {
                // 新的按键序列
                if (button_press_count == 1) {
                    // 单次短按:切换录制状态
                    if (!is_recording && !is_playing) {
                        recordAudio();
                    } else if (is_recording) {
                        // 如果正在录制,停止录制
                        is_recording = false;
                        digitalWrite(LED_PIN, LOW);
                        Serial.println("录制已停止");
                    }
                } else if (button_press_count == 2) {
                    // 双击:清除录制内容
                    if (audio_buffer != nullptr) {
                        free(audio_buffer);
                        audio_buffer = nullptr;
                        total_samples_recorded = 0;
                        Serial.println("录制内容已清除");
                    }
                }
            }
        } else {
            // 长按:播放
            if (!is_recording && !is_playing && total_samples_recorded > 0) {
                playAudio();
            }
        }
        
        button_was_down = false;
    }
    
    // 重置按键计数
    if (millis() - last_button_time > 1000 && button_press_count > 0) {
        last_button_time = millis();
        button_press_count = 0;
    }
    
    // 状态指示灯闪烁(系统空闲时)
    static unsigned long last_blink_time = 0;
    static bool led_state = false;
    
    if (!is_recording && !is_playing) {
        if (millis() - last_blink_time > 1000) {
            led_state = !led_state;
            digitalWrite(LED_PIN, led_state);
            last_blink_time = millis();
        }
    }
    
    delay(10);  // 降低CPU占用
}



方案2:使用Arduino库的简化版本

如果需要更简单的实现,可以创建库目录结构:

lib/
└── AudioIO/
    ├── AudioIO.h
    └── AudioIO.cpp
    

lib/AudioIO/AudioIO.h:

#ifndef AUDIOIO_H
#define AUDIOIO_H

#include <Arduino.h>
#include <driver/i2s.h>

class AudioRecorder {
public:
   AudioRecorder(int ws_pin, int bck_pin, int din_pin, int dout_pin);
   
   bool begin(int sample_rate = 16000);
   bool record(int seconds, int16_t* buffer);
   bool play(int16_t* buffer, size_t sample_count);
   void end();
   
private:
   int _ws_pin, _bck_pin, _din_pin, _dout_pin;
   int _sample_rate;
   bool _initialized = false;
   
   bool setupI2S();
};

#endif

lib/AudioIO/AudioIO.cpp:

#include "AudioIO.h"

AudioRecorder::AudioRecorder(int ws_pin, int bck_pin, int din_pin, int dout_pin)
    : _ws_pin(ws_pin), _bck_pin(bck_pin), _din_pin(din_pin), _dout_pin(dout_pin) {}

bool AudioRecorder::begin(int sample_rate) {
    _sample_rate = sample_rate;
    return setupI2S();
}

bool AudioRecorder::setupI2S() {
    // I2S配置代码(类似上面的setupI2S函数)
    // ...
    _initialized = true;
    return true;
}

bool AudioRecorder::record(int seconds, int16_t* buffer) {
    if (!_initialized || buffer == nullptr) return false;
    
    size_t total_samples = _sample_rate * seconds;
    size_t samples_recorded = 0;
    
    while (samples_recorded < total_samples) {
        size_t bytes_read;
        i2s_read(I2S_NUM_0, buffer + samples_recorded, 
                min(1024, (int)(total_samples - samples_recorded)) * sizeof(int16_t),
                &bytes_read, portMAX_DELAY);
        samples_recorded += bytes_read / sizeof(int16_t);
    }
    
    return true;
}

bool AudioRecorder::play(int16_t* buffer, size_t sample_count) {
    if (!_initialized || buffer == nullptr) return false;
    
    size_t samples_played = 0;
    
    while (samples_played < sample_count) {
        size_t bytes_written;
        i2s_write(I2S_NUM_1, buffer + samples_played,
                 min(1024, (int)(sample_count - samples_played)) * sizeof(int16_t),
                 &bytes_written, portMAX_DELAY);
        samples_played += bytes_written / sizeof(int16_t);
    }
    
    return true;
}

void AudioRecorder::end() {
    i2s_driver_uninstall(I2S_NUM_0);
    i2s_driver_uninstall(I2S_NUM_1);
    _initialized = false;
}

简化主程序 (src/main.cpp):

#include <Arduino.h>
#include "AudioIO.h"

#define RECORD_SECONDS 3
#define MAX_SAMPLES (16000 * RECORD_SECONDS)

AudioRecorder recorder(25, 26, 33, 22);
int16_t audio_buffer[MAX_SAMPLES];

void setup() {
    Serial.begin(115200);
    delay(1000);
    
    if (recorder.begin(16000)) {
        Serial.println("音频系统初始化成功");
    } else {
        Serial.println("音频系统初始化失败");
        while(1);
    }
}

void loop() {
    Serial.println("开始录制...");
    if (recorder.record(RECORD_SECONDS, audio_buffer)) {
        Serial.println("录制完成,开始播放...");
        delay(1000);
        
        recorder.play(audio_buffer, MAX_SAMPLES);
        Serial.println("播放完成");
    }
    
    delay(2000);
}