34、婴儿监视器应用开发:从音频检测到内存管理

婴儿监视器应用开发:从音频检测到内存管理

在开发一个婴儿监视器应用时,我们需要实现三个关键类: AppAudio AppRmaker AppMem 。下面将详细介绍这些类的实现过程。

1. AppAudio 类的实现

AppAudio 类主要负责婴儿哭声的检测。由于音频处理和机器学习推理需要大量内存,而 ESP32 的内部 RAM 无法满足需求,因此我们需要将一些缓冲区和任务栈移动到 ESP32 - S3 模块的 SPIRAM 中。

以下是 main/AppAudio.hpp 头文件中 AppAudio 类的实现:

#pragma once
#include <cstring>
#include <mutex>
#include <functional>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "edge-impulse-sdk/classifier/ei_run_classifier.h"
#include "dl_lib_coefgetter_if.h"
#include "esp_afe_sr_models.h"
#include "esp_board_init.h"
#include "model_path.h"

namespace app
{
    class AppAudio
    {
    private:
        static constexpr int CRYING_IDX{0};
        static constexpr int NOISE_IDX{1};
        static constexpr int AUDIO_BUFFER_SIZE{16000};
        float *m_audio_buffer;
        float *m_features;
        int m_audio_buffer_ptr{0};
        std::mutex m_features_mutex;

        static constexpr int DETECT_TASK_BUFF_SIZE{4 * 1024};
        inline static uint8_t *m_detect_task_buffer;
        inline static StaticTask_t m_detect_task_data;
        static constexpr int ACTION_TASK_BUFF_SIZE{4 * 1024};
        inline static uint8_t *m_action_task_buffer;
        inline static StaticTask_t m_action_task_data;

        static constexpr int FEED_TASK_BUFF_SIZE{8 * 1024};
        inline static uint8_t m_feed_task_buffer[FEED_TASK_BUFF_SIZE];
        inline static StaticTask_t m_feed_task_data;

        esp_afe_sr_iface_t *m_afe_handle{nullptr};
        esp_afe_sr_data_t *m_afe_data{nullptr};

        std::function<void(bool)> m_crying_fn;
        bool m_crying;

        static void feedTask(void *arg);
        static void detectTask(void *arg);
        static void actionTask(void *arg);
        static afe_config_t defaultAfeConfig();

    public:
        void init(std::function<void(bool)> f)
        {
            m_crying_fn = f;
            m_crying = false;
            m_audio_buffer = new float[AUDIO_BUFFER_SIZE];
            m_features = new float[AUDIO_BUFFER_SIZE];

            m_detect_task_buffer = new uint8_t[DETECT_TASK_BUFF_SIZE];
            m_action_task_buffer = new uint8_t[ACTION_TASK_BUFF_SIZE];

            esp_board_init(AUDIO_HAL_16K_SAMPLES, 1, 16);
            m_afe_handle = const_cast<esp_afe_sr_iface_t *>(&ESP_AFE_VC_HANDLE);
            afe_config_t afe_config = defaultAfeConfig();
            m_afe_data = m_afe_handle->create_from_config(&afe_config);
        }

        void start(void)
        {
            xTaskCreateStaticPinnedToCore(feedTask, "feed",
                                          FEED_TASK_BUFF_SIZE, this, 5, m_feed_task_buffer,
                                          &m_feed_task_data, 0);
            xTaskCreateStaticPinnedToCore(detectTask, "detect",
                                          DETECT_TASK_BUFF_SIZE, this, 5,
                                          m_detect_task_buffer, &m_detect_task_data, 1);
            xTaskCreateStaticPinnedToCore(actionTask, "action",
                                          ACTION_TASK_BUFF_SIZE, this, 5,
                                          m_action_task_buffer, &m_action_task_data, 1);
        }
    };

    void AppAudio::feedTask(void *arg)
    {
        AppAudio *obj{static_cast<AppAudio *>(arg)};
        int audio_chunksize = obj->m_afe_handle->get_feed_chunksize(obj->m_afe_data);
        int feed_channel = esp_get_feed_channel();
        int16_t *i2s_buff = new int16_t[audio_chunksize * feed_channel];

        while (true)
        {
            esp_get_feed_data(false, i2s_buff, audio_chunksize * sizeof(int16_t) * feed_channel);
            obj->m_afe_handle->feed(obj->m_afe_data, i2s_buff);
        }
    }

    void AppAudio::detectTask(void *arg)
    {
        AppAudio *obj{static_cast<AppAudio *>(arg)};
        int afe_chunksize{obj->m_afe_handle->get_fetch_chunksize(obj->m_afe_data)};
        while (true)
        {
            afe_fetch_result_t *res = obj->m_afe_handle->fetch(obj->m_afe_data);
            if (res == nullptr || res->ret_value == ESP_FAIL)
            {
                continue;
            }
            for (int i = 0; i < afe_chunksize; ++i)
            {
                obj->m_audio_buffer_ptr %= AUDIO_BUFFER_SIZE;
                obj->m_audio_buffer[obj->m_audio_buffer_ptr++] = res->data[i];
            }

            {
                std::lock_guard<std::mutex> guard(obj->m_features_mutex);
                for (int i = 0; i < AUDIO_BUFFER_SIZE; ++i)
                {
                    obj->m_features[i] = obj->m_audio_buffer[(obj->m_audio_buffer_ptr + i) % AUDIO_BUFFER_SIZE];
                }
            }
        }
    }

    void AppAudio::actionTask(void *arg)
    {
        AppAudio *obj{static_cast<AppAudio *>(arg)};
        ei_impulse_result_t result = {nullptr};
        auto get_data_fn = [&obj](size_t offset, size_t length, float *out_ptr) -> int
        {
            memcpy(out_ptr, obj->m_features + offset, length * sizeof(float));
            return 0;
        };

        while (true)
        {
            signal_t features_signal{get_data_fn, AUDIO_BUFFER_SIZE};
            int result_idx{NOISE_IDX};
            {
                std::lock_guard<std::mutex> guard(obj->m_features_mutex);
                if (run_classifier(&features_signal, &result) == EI_IMPULSE_OK)
                {
                    for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; ++i)
                    {
                        if (result.classification[i].value > result.classification[result_idx].value)
                        {
                            result_idx = i;
                        }
                    }
                }
            }

            switch (result_idx)
            {
            case CRYING_IDX:
            {
                if (!obj->m_crying)
                {
                    obj->m_crying_fn(true);
                    obj->m_crying = true;
                }
            }
            break;
            case NOISE_IDX:
            {
                if (obj->m_crying)
                {
                    obj->m_crying_fn(false);
                    obj->m_crying = false;
                }
            }
            break;
            default:
                break;
            }
            vTaskDelay(pdMS_TO_TICKS(1000));
        }
    }

    afe_config_t AppAudio::defaultAfeConfig()
    {
        return {
            .aec_init = false,
            .se_init = true,
            .vad_init = false,
            .wakenet_init = false,
            .voice_communication_init = true,
            .voice_communication_agc_init = false,
            .voice_communication_agc_gain = 15,
            .vad_mode = VAD_MODE_3,
            .wakenet_model_name = nullptr,
            .wakenet_mode = DET_MODE_2CH_90,
            .afe_mode = SR_MODE_LOW_COST,
            .afe_perferred_core = 0,
            .afe_perferred_priority = 5,
            .afe_ringbuf_size = 50,
            .memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM,
            .agc_mode = AFE_MN_PEAK_AGC_MODE_2,
            .pcm_config = {3, 2, 1, 16000},
            .debug_init = false,
        };
    }
}

以下是 AppAudio 类的主要功能和实现步骤:
- 成员变量定义
- CRYING_IDX NOISE_IDX :表示推理结果的索引。
- m_audio_buffer m_features :音频处理缓冲区,需要分配到外部 RAM。
- 多个任务缓冲区和任务数据结构,用于 FreeRTOS 任务。
- m_afe_handle m_afe_data :AFE 相关句柄。
- m_crying_fn :新哭声事件的回调函数。
- m_crying :表示最后一次 ML 推理结果是否为哭声事件。
- init 函数
- 保存回调函数 m_crying_fn
- 分配 m_audio_buffer m_features 到外部 RAM。
- 分配检测和动作任务的栈到外部 RAM。
- 初始化开发板和 AFE。
- start 函数
- 创建三个 FreeRTOS 任务: feedTask detectTask actionTask
- feedTask 函数
- 从音频子系统获取原始音频数据。
- 将数据传递给 AFE 进行处理。
- detectTask 函数
- 从 AFE 获取预处理后的单通道数据。
- 将数据复制到 m_audio_buffer 中。
- 将 m_audio_buffer 中的数据复制到 m_features 中。
- actionTask 函数
- 运行 ML 推理。
- 根据推理结果调用回调函数。
- defaultAfeConfig 函数
- 返回 AFE 的配置,使用更多的 PSRAM 以节省内部 RAM。

2. AppRmaker 类的实现

AppRmaker 类用于与 ESP RainMaker 平台集成,实现婴儿哭声状态的上报和通知功能。

以下是 main/AppRmaker.hpp 头文件中 AppRmaker 类的实现:

#pragma once
#include <cstring>
#include <cstdint>
#include <functional>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include <esp_rmaker_core.h>
#include <esp_rmaker_standard_types.h>
#include <esp_rmaker_standard_params.h>
#include <esp_rmaker_standard_devices.h>
#include <esp_rmaker_common_events.h>
#include <esp_rmaker_utils.h>
#include <esp_rmaker_mqtt.h>

namespace app
{
    class AppRmaker
    {
    private:
        esp_rmaker_node_t *m_rmaker_node;
        esp_rmaker_device_t *m_device;
        esp_rmaker_param_t *m_cry_param;

    public:
        void init()
        {
            esp_rmaker_time_set_timezone(CONFIG_ESP_RMAKER_DEF_TIMEZONE);
            esp_rmaker_config_t rainmaker_cfg = {
                .enable_time_sync = true,
            };
            m_rmaker_node = esp_rmaker_node_init(&rainmaker_cfg, "Baby node", "esp.node.sensor");

            m_device = esp_rmaker_device_create("Cry sensor", "esp.device.sensor", (void *)this);
            esp_rmaker_device_add_param(m_device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, "Cry sensor"));

            m_cry_param = esp_rmaker_param_create("Baby crying", "esp.param.toggle", esp_rmaker_bool(false), PROP_FLAG_READ);
            esp_rmaker_param_add_ui_type(m_cry_param, ESP_RMAKER_UI_TOGGLE);
            esp_rmaker_device_add_param(m_device, m_cry_param);
            esp_rmaker_device_assign_primary_param(m_device, m_cry_param);

            esp_rmaker_node_add_device(m_rmaker_node, m_device);
        }

        void start()
        {
            esp_rmaker_start();
        }

        void update(bool state)
        {
            esp_rmaker_param_update_and_report(m_cry_param, esp_rmaker_bool(state));
            if (state)
            {
                esp_rmaker_raise_alert("crying");
            }
        }
    };
}

以下是 AppRmaker 类的主要功能和实现步骤:
- 成员变量定义
- m_rmaker_node :RainMaker 节点。
- m_device :RainMaker 设备。
- m_cry_param :婴儿哭声状态参数。
- init 函数
- 设置时区并启用时间同步。
- 创建 RainMaker 节点。
- 创建 RainMaker 设备并添加名称参数。
- 创建婴儿哭声状态参数并添加到设备中。
- 将设备添加到节点中。
- start 函数
- 启动 RainMaker 代理。
- update 函数
- 更新婴儿哭声状态参数。
- 如果状态为哭声,则触发警报。

3. AppMem 类的实现

AppMem 类用于监控内部和外部堆的使用情况,帮助我们了解应用程序在运行时的内存使用情况。

以下是 main/AppMem.hpp 头文件中 AppMem 类的实现:

#pragma once
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_heap_caps.h"

namespace app
{
    class AppMem
    {
    private:
        constexpr static const char *TAG{"app-mem"};
        esp_timer_handle_t m_periodic_timer;

        static void periodic_timer_callback(void *arg)
        {
            ESP_LOGI(TAG, "------- mem stats -------");
            ESP_LOGI(TAG, "internal\t: %10u (free) / %10u(total)",
                     heap_caps_get_free_size(MALLOC_CAP_INTERNAL), heap_caps_get_total_size(MALLOC_CAP_INTERNAL));
            ESP_LOGI(TAG, "spiram\t: %10u (free) / %10u (total)",
                     heap_caps_get_free_size(MALLOC_CAP_SPIRAM), heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
        }

    public:
        void monitor(void)
        {
            const esp_timer_create_args_t periodic_timer_args = {
                .callback = periodic_timer_callback,
                .arg = this};

            ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &m_periodic_timer));
            ESP_ERROR_CHECK(esp_timer_start_periodic(m_periodic_timer, 5000000u));
        }

        void print(void)
        {
            periodic_timer_callback(nullptr);
        }
    };
}

以下是 AppMem 类的主要功能和实现步骤:
- 成员变量定义
- TAG :日志标签。
- m_periodic_timer :周期性定时器。
- periodic_timer_callback 函数
- 打印内部和外部堆的使用情况。
- monitor 函数
- 创建周期性定时器。
- 启动定时器,每 5 秒执行一次回调函数。
- print 函数
- 手动打印堆的使用情况。

4. 应用程序的整合

最后,我们需要在 main/main.cpp 源文件中整合这些类:

#include "AppAudio.hpp"
#include "AppDriver.hpp"
#include "AppRmaker.hpp"
#include "AppMem.hpp"

namespace
{
    app::AppAudio app_audio;
    app::AppDriver app_driver;
    app::AppRmaker app_rmaker;
    app::AppMem app_mem;
}

总结

通过实现 AppAudio AppRmaker AppMem 类,我们完成了一个婴儿监视器应用的开发。 AppAudio 类负责音频处理和哭声检测, AppRmaker 类负责与 ESP RainMaker 平台集成, AppMem 类负责监控内存使用情况。这些类的协同工作使得我们能够实时检测婴儿的哭声,并通过 RainMaker 平台进行状态上报和通知。

以下是整个应用程序的流程图:

graph TD;
    A[开始] --> B[初始化 AppAudio];
    B --> C[初始化 AppRmaker];
    C --> D[初始化 AppMem];
    D --> E[启动 AppAudio 任务];
    E --> F[启动 AppRmaker 代理];
    F --> G[启动 AppMem 监控];
    G --> H[运行应用程序];
    H --> I{检测到哭声?};
    I -- 是 --> J[更新 AppRmaker 状态并触发警报];
    I -- 否 --> H;
    J --> H;

表格:类功能总结

类名 功能
AppAudio 音频处理和哭声检测
AppRmaker 与 ESP RainMaker 平台集成,状态上报和通知
AppMem 监控内部和外部堆的使用情况

通过以上步骤,我们可以开发出一个功能完善的婴儿监视器应用,实现音频检测、云平台集成和内存监控等功能。

婴儿监视器应用开发:从音频检测到内存管理

5. 详细功能分析
5.1 AppAudio 类功能深入剖析
  • 音频数据处理流程
    • 首先, feedTask 函数从音频子系统获取原始音频数据,这是整个音频处理的起点。它通过 esp_get_feed_data 函数获取数据,并将其传递给 AFE(音频前端)进行处理。AFE 会对数据进行滤波和通道合并等操作,将其转换为归一化的单通道数据。
    • 接着, detectTask 函数从 AFE 中获取预处理后的单通道数据。它将这些数据存储在 m_audio_buffer 这个循环缓冲区中,当缓冲区满时会自动回绕。然后,将 m_audio_buffer 中的数据复制到 m_features 中,为后续的 ML 推理做准备。
    • 最后, actionTask 函数使用 m_features 中的数据进行 ML 推理。它调用 run_classifier 函数进行推理,并根据推理结果调用回调函数 m_crying_fn ,通知外部有新的哭声事件发生。
  • 内存管理策略
    • AppAudio 类中,为了节省内部 RAM,将 m_audio_buffer m_features 分配到外部 RAM。通过在 sdkconfig 中配置相关选项,使得 malloc 函数在分配超过 1KB 的内存时从外部 RAM 分配。同时,检测和动作任务的栈也分配到外部 RAM,而 feedTask 的栈则分配在内部 RAM,这是因为 AFE 函数在获取音频数据时使用外部内存作为任务栈会导致崩溃。
5.2 AppRmaker 类功能深入剖析
  • RainMaker 集成流程
    • init 函数是集成的关键步骤。它首先设置时区并启用时间同步,然后创建 RainMaker 节点和设备。接着,创建婴儿哭声状态参数,并将其添加到设备中,最后将设备添加到节点中。
    • start 函数启动 RainMaker 代理,使得设备能够与 RainMaker 平台进行通信。
    • update 函数用于更新婴儿哭声状态参数,并在检测到哭声时触发警报,通过 esp_rmaker_raise_alert 函数通知用户。
  • 用户交互设计
    • 婴儿哭声参数在 RainMaker 移动应用的 GUI 上显示为一个只读的布尔值,以切换开关的形式呈现,方便用户直观地查看婴儿的状态。
5.3 AppMem 类功能深入剖析
  • 内存监控机制
    • monitor 函数创建一个周期性定时器,每 5 秒执行一次 periodic_timer_callback 函数。该回调函数会打印内部和外部堆的使用情况,让开发者实时了解应用程序的内存使用状态。
    • print 函数允许开发者手动打印堆的使用情况,方便在特定时刻进行内存检查。
6. 代码优化建议
6.1 AppAudio 类优化
  • 缓冲区管理优化 :可以考虑使用更高效的缓冲区管理算法,减少内存碎片。例如,采用环形缓冲区的动态调整策略,根据实际音频数据的流量调整缓冲区大小。
  • 任务调度优化 :可以根据音频处理的实时性要求,调整 FreeRTOS 任务的优先级和调度策略,确保音频数据的及时处理。
6.2 AppRmaker 类优化
  • 网络通信优化 :可以采用更高效的网络协议或优化 MQTT 通信参数,减少通信延迟和带宽消耗。
  • 错误处理优化 :增加对 RainMaker 平台通信错误的处理机制,提高系统的稳定性。
6.3 AppMem 类优化
  • 监控频率调整 :根据应用程序的实际运行情况,动态调整内存监控的频率,避免不必要的资源消耗。
7. 实际应用场景分析
7.1 家庭使用场景
  • 在家庭环境中,该婴儿监视器应用可以实时监测婴儿的哭声。当婴儿哭泣时,通过 RainMaker 平台的通知功能,家长可以及时收到警报,了解婴儿的状态。同时,内存监控功能可以确保应用程序在长时间运行时的稳定性,避免因内存泄漏等问题导致系统崩溃。
7.2 公共场所使用场景
  • 在托儿所、幼儿园等公共场所,多个婴儿监视器可以同时使用。通过 RainMaker 平台的集中管理功能,工作人员可以实时监控所有婴儿的状态,提高管理效率。

总结与展望

8. 总结

通过实现 AppAudio AppRmaker AppMem 类,我们成功开发了一个功能完善的婴儿监视器应用。该应用实现了音频处理和哭声检测、与 ESP RainMaker 平台的集成以及内存使用情况的监控。各个类之间协同工作,确保了应用程序的稳定性和可靠性。

9. 展望
  • 功能扩展 :可以添加更多的音频特征分析功能,如哭声的强度、频率等,为家长提供更详细的信息。
  • 智能化升级 :结合人工智能技术,实现对婴儿哭声的分类,例如区分饥饿、困倦、不适等不同原因的哭声。
  • 用户体验优化 :进一步优化 RainMaker 平台的用户界面,提供更友好的交互体验。

表格:优化建议总结

类名 优化方向 具体建议
AppAudio 缓冲区管理 使用更高效的缓冲区管理算法,动态调整缓冲区大小
AppAudio 任务调度 根据实时性要求调整任务优先级和调度策略
AppRmaker 网络通信 采用更高效的网络协议,优化 MQTT 通信参数
AppRmaker 错误处理 增加对 RainMaker 平台通信错误的处理机制
AppMem 监控频率 动态调整内存监控的频率

流程图:应用程序优化流程

graph TD;
    A[开始] --> B[分析应用程序性能];
    B --> C{是否需要优化?};
    C -- 是 --> D[选择优化方向];
    D --> E[实施优化措施];
    E --> F[测试优化效果];
    F --> G{优化是否成功?};
    G -- 是 --> H[结束];
    G -- 否 --> D;
    C -- 否 --> H;

通过以上的分析和优化建议,我们可以进一步完善婴儿监视器应用,使其在实际应用中发挥更大的作用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值