Skip to main content

使用 Edge Impulse 与 reSpeaker XVF3800 实现 TinyML 语音识别

概述

通过 TinyML 驱动的关键词检测(KWS)系统,实现实时语音命令检测与免手操作控制。将高性能的 ReSpeaker XVF3800 麦克风阵列与高效的 XIAO ESP32S3 以及 Edge Impulse 平台相结合,我们为小型、低功耗设备带来了语音识别能力。训练、部署并开始聆听——你的设备时刻准备响应你的下一条指令!

所需硬件

pir

数据采集

为带有 XIAO ESP32S3 的 ReSpeaker XVF3800 安装 USB 固件

在开始音频数据采集之前,请确保你的 ReSpeaker 已经烧录了 USB 固件,使其能够作为 USB 麦克风工作。

固件安装 Wiki

固件文件

配置 Python 环境

然后你需要在笔记本电脑或台式机上搭建 python 环境以采集语音数据。这里我们将创建 respeaker-env

python -m venv respeaker-env
source respeaker-env/bin/activate

安装所需库:

pip install sounddevice scipy numpy 

查找 ReSpeaker 设备 ID

为了从正确的麦克风输入进行录音,我们需要识别 ReSpeaker 麦克风的设备索引。

import sounddevice as sd

devices = sd.query_devices()
for i, device in enumerate(devices):
print(f"Device {i}: {device['name']} (input channels: {device['max_input_channels']})")

查找与 ReSpeaker 对应的设备名称(通常类似 ReSpeaker XVF3800 USB 4-Mic Array),并记下其索引号(例如 Device 2)。

录制音频样本

下面的脚本可以帮助你录制带标签的音频样本,并按人员和命令/关键词进行组织。


import os
import sounddevice as sd
from scipy.io.wavfile import write

# === Settings ===
SAMPLERATE = 16000
CHANNELS = 1 # Mono input
DURATION = 10 # seconds
DEVICE_INDEX = 2 # Replace with correct device index

def record_audio(filename, samplerate=SAMPLERATE, channels=CHANNELS, duration=DURATION, device=DEVICE_INDEX):
print(f"Recording '{filename}' for {duration} seconds...")
recording = sd.rec(int(duration * samplerate),
samplerate=samplerate,
channels=channels,
dtype='int32',
device=device)
sd.wait()
write(filename, samplerate, recording)
print(f"Saved: {filename}")

def get_next_filename(directory, label):
existing = [f for f in os.listdir(directory) if f.startswith(label) and f.endswith('.wav')]
index = len(existing) + 1
return os.path.join(directory, f"{label}.{index}.wav")

def collect_samples():
while True:
sample_name = input("Enter sample name (e.g., PersonA): ").strip()
if not sample_name:
print("Sample name cannot be empty.")
continue

sample_dir = os.path.join(os.getcwd(), sample_name)
os.makedirs(sample_dir, exist_ok=True)
print(f"Directory created: {sample_dir}")

while True:
label = input("Enter sound/voice to record (e.g., yes, no): ").strip()
if not label:
print("Label cannot be empty.")
continue

while True:
filename = get_next_filename(sample_dir, label)
record_audio(filename)

cont = input("Record another sample for this label? (yes/no): ").strip().lower()
if cont != 'yes':
break

next_label = input("Do you want to record a different label? (yes/no): ").strip().lower()
if next_label != 'yes':
break

next_sample = input("Do you want to create a new sample? (yes/no): ").strip().lower()
if next_sample != 'yes':
print("Audio collection completed.")
break

if __name__ == "__main__":
collect_samples()

文件夹结构示例

/PersonA
├── red.1.wav
├── red.2.wav
├── blue.1.wav
└── blue.2.wav
/PersonB
├── red.1.wav
└── green.1.wav

每个人的文件夹中都包含带标签的 .wav 文件,这些文件稍后将被上传到 Edge Impulse 进行模型训练。

在 Edge Impulse 中上传和准备音频数据

使用 ReSpeaker XVF3800 采集原始音频样本并按标签进行整理后,下一步是在 Edge Impulse Studio 中上传和处理这些数据,以训练你的关键词检测模型。

在 Edge Impulse 中创建新项目

  • 访问 Edge Impulse 并登录(如果你是新用户,请先注册)。

  • 点击 "Create new project"。

  • 为你的项目命名(例如:"Voice Command KWS")

上传已有音频样本

要上传你采集的数据:

  • 1.导航到 Data Acquisition 选项卡。
  • 2.点击右上角的 "Upload existing data"
  • 3.选择并上传包含 .wav 文件的文件夹

pir

  • 4.启用自动将数据划分为训练集和测试集的选项(Edge Impulse 推荐约 80/20 划分)。

将 10 秒音频拆分为 1 秒样本

Edge Impulse 在处理关键词检测时,使用 1 秒音频片段效果最佳。由于原始样本是以 10 秒为单位录制的,你需要将每个样本拆分为多个 1 秒样本。

请按以下步骤操作

  • 1.上传完成后,进入 Data Acquisition 页面。
  • 2.找到一个样本(例如 yes.1.wav),点击该样本旁边的三个 点 (…)
  • 3.在菜单中选择 "Split sample"。

pir

  • 4.使用工具将波形拆分为 1 秒片段。
    • a.你可以拖动以调整片段,或根据需要添加/删除片段。
  • 5.点击 Save and Split。

对所有类别中每一个 10 秒样本(包括训练集和测试集)重复上述过程。

这样可以确保你的数据集格式正确,并针对训练高精度模型进行了优化。

pir

创建 Impulse(预处理 / 模型定义)

在 Edge Impulse 中,impulse 定义了将原始数据转换为已训练机器学习模型的端到端流程。它包括 信号处理特征提取,以及用于分类的 学习模块

创建 Impulse

  • 1.在 Edge Impulse 项目中导航到 "Impulse Design" 选项卡。
  • 2.点击 “Create Impulse”
  • 3.配置输入窗口:
    • a.Window size:1000 ms(1 秒)
    • b.Window increase:500 ms(通过重叠窗口增强数据)
    • c.启用 "Zero-pad data":这可以确保较短的片段(例如 800ms)用零填充,在样本拆分时启用噪声裁剪时尤其有用。

添加 MFCC 特征提取器

创建好 impulse 窗口后:

  • 1.点击 “Add a processing block” 并选择 MFCC (Mel Frequency Cepstral Coefficients)
    • a.MFCC 是一种广泛使用的方法,用于将音频信号转换为表示语音频率模式的二维特征。
    • b.这些特征非常适合用于基于语音的识别模型。
  • 2.设置 MFCC 参数(默认设置在大多数情况下都表现良好):
    • a.Output shape:13 x 49 x 1
    • b.这会将你的音频片段转换为用于分类的“图像”。

添加学习模块

  • 1.点击 “Add a learning block” 并选择 “Classification (Keras)”
  • 2.这将创建一个自定义的 卷积神经网络(CNN),用于对 MFCC 特征进行 图像分类
  • 3.现在你可以进入 NN Classifier 选项卡,自定义并训练你的模型。

pir

预处理(MFCC)

下一步是从录制的音频中生成频谱图图像,这些图像将用于模型训练。我们可以使用默认的 DSP 参数,或者像本项目一样,利用 DSP Autotune 功能自动优化参数,以获得更好的性能。

pir

构建机器学习模型

在本项目中,我们将使用卷积神经网络(CNN)模型。其结构由两个 Conv1D + MaxPooling 层组成,分别具有 8 和 16 个滤波器,之后是一个 0.25 的 Dropout 层。展平(Flatten)后,最终的全连接层包含四个神经元——每个类别对应一个神经元。 我们将使用 0.005 的学习率训练 100 个 epoch。为了提升泛化能力和鲁棒性,将应用背景噪声等数据增强技术。初步结果非常有前景。

pir

部署到搭载 XIAO ESP32 S3 的 ReSpeaker XVF3800

Edge Impulse 将自动把所有必要的库、预处理函数和训练好的模型打包成一个可下载的包。 继续操作:

  • 1.选择 "Arduino Library" 作为部署选项。
  • 2.在底部选择 "Quantized (Int8)" 格式。
  • 3.点击 "Build" 生成库。

pir

下载完成后:

  • 4.打开 Arduino IDE,进入 Sketch 菜单。
  • 5.选择 "Include Library" > "Add .ZIP Library..."
  • 6.选择从 Edge Impulse 下载的 .zip 文件,将其添加到你的 Arduino 项目中。

将固件切换到 I2S 模式

在上传 Arduino 代码之前,你必须将 ReSpeaker XVF3800 固件切换到 I2S 模式,以启用通过 I2C 协议进行通信。 Firmware Installation Guide

Arduino 代码集成

Edge Impulse 提供的 Arduino 代码需要进行一些修改,以确保与 ReSpeaker XVF3800 和 XIAO ESP32S3 硬件兼容:根据你的设置更新 GPIO 引脚定义、I2S 采样率 以及其他与硬件相关的参数。

#define EIDSP_QUANTIZE_FILTERBANK   0
#include <Kasun9603-project-1_inferencing.h> // Change with your one
#include "driver/i2s.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// ==== AUDIO CONFIG ====
#define I2S_PORT I2S_NUM_0
#define I2S_WS 7 // L/R clock
#define I2S_SD 43 // Serial Data In
#define I2S_SCK 8 // Bit Clock

#define SAMPLE_RATE 16000
#define I2S_SAMPLE_BITS 32
#define SAMPLE_BUFFER_SIZE 2048

// ==== INFERENCE STATE ====
typedef struct {
int16_t *buffer;
uint8_t buf_ready;
uint32_t buf_count;
uint32_t n_samples;
} inference_t;

static inference_t inference;
static int32_t i2s_samples[SAMPLE_BUFFER_SIZE];
static bool record_status = true;
static bool debug_nn = false;

// ==== FUNCTION DECLARATIONS ====
static bool microphone_inference_start(uint32_t n_samples);
static bool microphone_inference_record(void);
static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr);
static void audio_inference_callback(uint32_t n_bytes);
static void capture_samples(void *arg);
static int i2s_init();
static void i2s_deinit();

void setup() {
Serial.begin(115200);
while (!Serial);

ei_printf("XVF3800 Keyword Spotting Inference Start\n");

ei_printf("Model info:\n");
ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
ei_printf("\tSample length: %d ms\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / (SAMPLE_RATE / 1000));
ei_printf("\tInterval: %.2f ms\n", EI_CLASSIFIER_INTERVAL_MS);

if (!microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT)) {
ei_printf("ERR: Audio buffer allocation failed.\n");
return;
}

ei_printf("Listening...\n");
}

void loop() {
if (!microphone_inference_record()) {
ei_printf("ERR: Failed to record audio.\n");
return;
}

signal_t signal;
signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
signal.get_data = &microphone_audio_signal_get_data;

ei_impulse_result_t result = {0};
EI_IMPULSE_ERROR r = run_classifier(&signal, &result, debug_nn);

if (r != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", r);
return;
}

ei_printf("Predictions:\n");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(" %s: ", result.classification[ix].label);
ei_printf_float(result.classification[ix].value);
ei_printf("\n");
}

#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" Anomaly score: ");
ei_printf_float(result.anomaly);
ei_printf("\n");
#endif
}

// ==== INFERENCE AND AUDIO HANDLING ====

static void audio_inference_callback(uint32_t n_bytes) {
for (uint32_t i = 0; i < n_bytes / sizeof(int32_t); i++) {
int16_t val = i2s_samples[i] >> 16; // Convert from 32-bit signed to 16-bit
inference.buffer[inference.buf_count++] = val;

if (inference.buf_count >= inference.n_samples) {
inference.buf_ready = 1;
inference.buf_count = 0;
}
}
}

static void capture_samples(void *arg) {
size_t bytes_read;
while (record_status) {
i2s_read(I2S_PORT, (char *)i2s_samples, SAMPLE_BUFFER_SIZE * sizeof(int32_t), &bytes_read, portMAX_DELAY);

if (bytes_read > 0) {
audio_inference_callback(bytes_read);
} else {
ei_printf("ERR: I2S read failed\n");
}
}
vTaskDelete(NULL);
}

static bool microphone_inference_start(uint32_t n_samples) {
inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));
if (!inference.buffer) return false;

inference.buf_count = 0;
inference.n_samples = n_samples;
inference.buf_ready = 0;

if (i2s_init() != 0) {
ei_printf("ERR: I2S init failed\n");
return false;
}

xTaskCreate(capture_samples, "CaptureSamples", 4096, NULL, 1, NULL);
return true;
}

static bool microphone_inference_record(void) {
while (!inference.buf_ready) {
delay(10);
}
inference.buf_ready = 0;
return true;
}

static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr) {
numpy::int16_to_float(&inference.buffer[offset], out_ptr, length);
return 0;
}

static int i2s_init() {
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = (i2s_bits_per_sample_t)I2S_SAMPLE_BITS,
.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 = 512,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};

i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = -1,
.data_in_num = I2S_SD
};

esp_err_t err;
err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
if (err != ESP_OK) return err;

err = i2s_set_pin(I2S_PORT, &pin_config);
if (err != ESP_OK) return err;

err = i2s_zero_dma_buffer(I2S_PORT);
return err;
}

static void i2s_deinit() {
i2s_driver_uninstall(I2S_PORT);
}

技术支持与产品讨论

感谢你选择我们的产品!我们将为你提供多种支持,确保你在使用我们产品的过程中尽可能顺利。我们提供多种沟通渠道,以满足不同的偏好和需求。

Loading Comments...