Sakura
Published © Apache-2.0

Pet sound detection based on Seeed XIAO-esp32s3

Based on the Seeed XIAO-esp32s3, we have developed a pet sound detection system integrated with Home Assistant and Mi Home (MIJIA). This sys

IntermediateShowcase (no instructions)7 days366
Pet sound detection based on Seeed XIAO-esp32s3

Things used in this project

Hardware components

Seeed Studio XIAO ESP32S3 Sense
Seeed Studio XIAO ESP32S3 Sense
×1

Software apps and online services

Arduino IDE
Arduino IDE
Home Assistant
Home Assistant
MQTT
MQTT

Story

Read more

Schematics

edge impluse 训练好的模型

可以通过arduino直接部署在xiao-esp32s3上

Code

Pet sound detection based on Seeed XIAO-esp32s3

C/C++
arduino代码
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
//如果您的目标内存有限,请移除此宏以节省10KB的RAM
// If your target is limited in memory remove this macro to save 10K RAM

//定义EIDSP_QUANTIZE_FILTERBANK为0
#define EIDSP_QUANTIZE_FILTERBANK   0

// WiFi连接信息
const char* ssid = "K40";
const char* password = "heliangji";


//#include <XIAO-ESP32S3-KWS_inferencing.h>
#include <llly-project-1_inferencing.h>
#include <I2S.h>
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16

#define LED_BUILT_IN 21 
// MQTT服务器信息
const char* mqttServer = "192.168.230.250";
const int mqttPort = 1883;
const char* mqttUsername = "admin";
const char* mqttPassword = "hlj20020511";

// 创建WiFi客户端和MQTT客户端实例
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);

// 设置WiFi连接函数
void setupWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("正在连接WiFi...");
  }
  Serial.println("已连接到WiFi");
}

// 设置MQTT连接函数
void setupMQTT() {
  mqttClient.setServer(mqttServer, mqttPort);
}

// 重新连接MQTT服务器函数
void reconnectMQTT() {
  while (!mqttClient.connected()) {
    Serial.println("正在连接到MQTT...");
    if (mqttClient.connect("ESP32Client", mqttUsername, mqttPassword)) {
      Serial.println("连接到MQTT");
    } else {
      Serial.print("MQTT连接失败,rc=");
      Serial.print(mqttClient.state());
      Serial.println(" 5秒钟后重试...");
      delay(5000);
    }
  }
}

/** 这段代码定义了一个音频处理的结构体,包括缓冲区、指针和选择器。 */
typedef struct {
    int16_t *buffer;  // 音频缓冲区指针
    uint8_t buf_ready;  // 缓冲区是否准备好的标志
    uint32_t buf_count; // 缓冲区中样本数量
    uint32_t n_samples;   // 缓冲区中样本数量
} inference_t;  // 推断结构体

static inference_t inference; // 静态推断变量
static const uint32_t sample_buffer_size = 2048;   // 静态样本缓冲区大小
static signed short sampleBuffer[sample_buffer_size];   // 静态样本缓冲区
static bool debug_nn = false;   // 设置为 true 可以查看生成的原始信号特征等
static bool record_status = true;  // 录制状态标志


/**
 * @brief      Arduino setup function
 */
void setup()
{
    Serial.begin(115200);
    setupWiFi();
    setupMQTT();
    /////////////////////////////////////////////////////////////////////////////////////////////////////
    // 在这里放置你的setup代码,以运行一次:
    Serial.begin(115200); // 以115200波特率开始串口通信
    // 注释掉以下行以取消等待USB连接(需要本机USB)
    while (!Serial);
    Serial.println("Edge Impulse Inferencing Demo");   // 打印"Edge Impulse Inferencing Demo"到串口

    pinMode(LED_BUILT_IN, OUTPUT); // 将引脚设置为输出
    digitalWrite(LED_BUILT_IN, HIGH); //关闭内置LED

    I2S.setAllPins(-1, 42, 41, -1, -1); //设置I2S引脚 
    //  初始化I2C若失败则无限循环
    if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
      Serial.println("Failed to initialize I2S!");
    while (1) ;
  }
    
    // summary of inferencing settings (from model_metadata.h)
    ei_printf("Inferencing settings:\n");
    ei_printf("\tInterval: ");
    ei_printf_float((float)EI_CLASSIFIER_INTERVAL_MS);
    ei_printf(" ms.\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 / 16);
    ei_printf("\tNo. of classes: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));

    ei_printf("\nStarting continious inference in 2 seconds...\n");
    ei_sleep(2000);

    if (microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT) == false) {
        //ERR: 无法分配音频缓冲区(大小 %d),这可能是由于您的模型的窗口长度
        ei_printf("ERR: Could not allocate audio buffer (size %d), this could be due to the window length of your model\r\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT);
        return;
    }
    //录音中.......
    ei_printf("Recording...\n");
    
}

/**
 * @brief      Arduino main function. Runs the inferencing loop.
 * 该函数是一个Arduino主函数,用于运行推断循环。首先,它通过调用 microphone_inference_record() 函数来录制音频。
 * 如果录制失败,则打印错误消息并返回。然后,它使用录制的音频信号调用 run_classifier() 函数进行推断,并将结果存储在 result 变量中。
 * 如果推断失败,则打印错误消息并返回。接下来,它打印推断结果,包括DSP时间、分类时间和异常时间。
 * 然后,它遍历所有类别,并打印每个类别的标签和值。如果某个类别的值大于之前的最大值,则更新最大值和对应的类别索引。
 * 最后,根据最大值的索引和值,控制LED的亮灭状态。如果异常检测功能被启用,则打印异常得分。
 */
void loop()
{
      // 检查MQTT连接状态,如果未连接则尝试重新连接
    if (!mqttClient.connected()) {
      reconnectMQTT();
    }
    // 处理MQTT消息
    mqttClient.loop();
    /*****************************************************************************************************************************/
    bool m = microphone_inference_record(); // 录制音频
    if (!m) {
        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;
    }

    int pred_index = 0;     // 初始化预测索引
    float pred_value = 0;   // 初始化预测值

    // 打印预测结果
    ei_printf("Predictions ");
    ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
        result.timing.dsp, result.timing.classification, result.timing.anomaly);
    ei_printf(": \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 (result.classification[ix].value > pred_value){
           pred_index = ix;
           pred_value = result.classification[ix].value;
      }
    }
    // 显示推断结果 开灯 关灯
    if ((pred_index == 2) && (pred_value > 0.8)){
//      digitalWrite(LED_BUILT_IN, LOW); //Turn on
      mqttClient.publish("dog bark", "环境音~");
    }
    else  if ((pred_index == 1) && (pred_value > 0.8)){
//      digitalWrite(LED_BUILT_IN, HIGH); //Turn off
      mqttClient.publish("dog bark", "狗狗在大叫呢!!!");
    }else  if ((pred_index == 0) && (pred_value > 0.8)){
      mqttClient.publish("dog bark", "狗狗在撒娇呢~");
    }

    
#if EI_CLASSIFIER_HAS_ANOMALY == 1
    ei_printf("    anomaly score: ");//异常得分
    ei_printf_float(result.anomaly);
    ei_printf("\n");
#endif
}
/*
# 音频推断回调函数
# 参数:
#   n_bytes:字节数
*/
static void audio_inference_callback(uint32_t n_bytes)
{
    //遍历字节数的一半
    for(int i = 0; i < n_bytes>>1; i++) {
        inference.buffer[inference.buf_count++] = sampleBuffer[i];  //将样本数据存储到推断缓冲区
        // 如果推断缓冲区已满
        if(inference.buf_count >= inference.n_samples) {
          inference.buf_count = 0;  //  重置推断缓冲区计数器
          inference.buf_ready = 1;  //  设置推断缓冲区已准备好
        }
    }
}

/*
该函数是一个任务函数,用于捕获I2S数据并进行处理。
它首先读取指定数量的I2S数据,并进行一些处理,然后调用audio_inference_callback函数进行推断。
如果record_status为真,则继续循环读取和处理数据,否则退出循环。
最后,该函数会删除自身任务。
*/

static void capture_samples(void* arg) {

  const int32_t i2s_bytes_to_read = (uint32_t)arg;
  size_t bytes_read = i2s_bytes_to_read;
  //循环捕获数据,直到停止记录状态
  while (record_status) {

    /* 一次性从I2S读取数据 - 为XIAO ESP2S3 Sense和I2S.h库修改 */
    // i2s_read((i2s_port_t)1, (void*)sampleBuffer, i2s_bytes_to_read, &bytes_read, 100);
    esp_i2s::i2s_read(esp_i2s::I2S_NUM_0, (void*)sampleBuffer, i2s_bytes_to_read, &bytes_read, 100);

    //  如果读取的字节数小于等于0,则输出错误信息
    if (bytes_read <= 0) {
      ei_printf("Error in I2S read : %d", bytes_read);
    }
    else {
        //  如果读取的字节数小于指定的字节数,则输出部分读取信息
        if (bytes_read < i2s_bytes_to_read) {
        ei_printf("Partial I2S read");
        }

        // 对数据进行缩放(否则声音太轻)
        for (int x = 0; x < i2s_bytes_to_read/2; x++) {
            sampleBuffer[x] = (int16_t)(sampleBuffer[x]) * 8;
        }
        //  如果记录状态为真,则调用音频推断回调函数
        if (record_status) {
            audio_inference_callback(i2s_bytes_to_read);
        }
        else {
            break;  //  如果记录状态为假,则跳出循环
        }
    }
  }
  vTaskDelete(NULL);  //释放任务
}

/**
 * @brief      初始化推断结构并设置/启动PDM
 *
 * @param[in]  n_samples  The n samples 样本数量
 *
   * @return     { description_of_the_return_value }返回描述返回值的描述
   * 该函数用于启动麦克风的推断过程。它接受一个参数n_samples,表示要推断的样本数量。
   * 函数首先分配了一个大小为n_samples的int16_t类型数组,并将其赋值给inference.buffer。
   * 如果分配失败,则返回false。然后,函数将inference.buf_count设置为0,inference.n_samples设置为n_samples,inference.buf_ready设置为0。
   * 接下来,函数调用ei_sleep函数等待100毫秒。
   * 最后,函数创建了一个名为CaptureSamples的任务,并返回true。
 */
static bool microphone_inference_start(uint32_t n_samples)
{
    inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));

    if(inference.buffer == NULL) {
        return false;
    }

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

//    if (i2s_init(EI_CLASSIFIER_FREQUENCY)) {
//        ei_printf("Failed to start I2S!");
//    }

    ei_sleep(100);

    record_status = true;

    xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, (void*)sample_buffer_size, 10, NULL);

    return true;
}

/**
 * @brief      Wait on new data
 *
 * @return     True when finished
 * 该函数是一个静态函数,用于等待新的数据。当inference.buf_ready为0时,函数会进入一个循环,每次循环延迟10毫秒。
 * 当inference.buf_ready不为0时,函数会将inference.buf_ready设置为0,并返回True。
 */
static bool microphone_inference_record(void)
{
    bool ret = true;

    while (inference.buf_ready == 0) {
        delay(10);
    }

    inference.buf_ready = 0;
    return ret;
}

/**
 * Get raw audio signal data
 * 该函数用于获取麦克风音频信号数据。它接受一个偏移量和长度参数,以及一个指向输出数据的指针。
 * 函数使用numpy库将指定偏移量和长度范围内的整数音频信号转换为浮点数信号,并将结果存储在输出指针指向的内存中。
 * 最后,函数返回0表示成功获取音频信号数据。
 */
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;
}

/**
 * @brief      Stop PDM and release buffers
 * 该函数用于停止PDM(Pulse Density Modulation)并释放缓冲区。它首先释放sampleBuffer,然后释放inference.buffer。
 */
static void microphone_inference_end(void)
{
    free(sampleBuffer);
    ei_free(inference.buffer);
}


#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_MICROPHONE
#error "Invalid model for current sensor."
#endif

Credits

Sakura

Sakura

1 project • 0 followers

Comments