前言:
此方法主要是使用乐鑫官方文档中提到的multinet模型进行离线命令词识别从而达到自定义唤醒词的效果 资源消耗会高很多。
乐鑫集成了WakeNet和MultiNet两个技术模块,目前开源代码使用的是WakeNet语音唤醒引擎,其好处是内存占比小(约20KB左右),计算速度快。 而本文中用到的是MultiNet轻量级模型,内存占用可能会WakeNet高好几倍,比其设计借鉴了卷积递归神经网络(CRNN)和连接主义时间分类(CTC),它可将音频的MFCC(Mel-Frequency Cepstral Coefficients)特征值处理为对应汉字或单词的组合音素,然后通过对比输出音素找到对应的语音命令。
本文不太适合小白,要有一点编程基础,其他基础配置小智官方文档里有
基于1.7.3版本源码修改(不同版本源码略有区别,但实现方式是一样的)
目前只支持汉语拼音,不支持汉语+英文(需要换其他库);
大佬们看个思路就行了
第一步:配置中加入汉语识别model
第二步:给model增加分区空间
根据自己芯片的flash大小修改对应的分区表(本文以16MB为例)
该model较大(2.3MB左右),原本默认的model分区不太够(具体多少够我也没试过,大佬们可以试一下)
通过减少ota_1的空间,分配给model,注意改偏移地址
第三步:在唤醒词初始化代码中加入multinet
找到这个函数 ,在其原本的模型加载和检测中加入一个新的模型加载和检测
void AfeWakeWord::Initialize(AudioCodec *codec)
{
codec_ = codec;
int ref_num = codec_->input_reference() ? 1 : 0;
srmodel_list_t *models = esp_srmodel_init("model"); // 初始化语音识别模型列表,从"model"目录加载
// 遍历所有加载的模型
for (int i = 0; i < models->num; i++)
{
ESP_LOGI(TAG, "Model %d: %s", i, models->model_name[i]); // 打印每个模型的名称
if (strstr(models->model_name[i], ESP_MN_PREFIX) != NULL) //此处 ESP_MN_PREFIX 为 "mn" 也就是新加的模型库
{
// 获取 multinet 模型句柄
multinet = esp_mn_handle_from_name(models->model_name[i]);
if (!multinet)
{
ESP_LOGI("MULTINET", "failed to create multinet handle");
continue;
}
//加载模型数据
model_data = multinet->create(models->model_name[i], 6000);
if (!model_data)
{
ESP_LOGI("MULTINET", "failed to create model_data handle");
continue;
}
}
if (model_data)
{
//此处使用拼音+空格的方式添加想要自定义的唤醒词
esp_mn_commands_clear();
esp_mn_commands_add(1, "ni hao xiao nuo");
esp_mn_commands_add(2, "xiao nuo xiao nuo");
esp_mn_commands_add(3, "xiao nuo");
esp_mn_commands_update();
}
esp_mn_active_commands_print(); //打印添加的唤醒词
if (strstr(models->model_name[i], ESP_WN_PREFIX) != NULL)
{
wakenet_model_ = models->model_name[i];
auto words = esp_srmodel_get_wake_words(models, wakenet_model_);
// split by ";" to get all wake words
std::stringstream ss(words);
std::string word;
while (std::getline(ss, word, ';'))
{
wake_words_.push_back(word);
}
}
}
std::string input_format;
for (int i = 0; i < codec_->input_channels() - ref_num; i++)
{
input_format.push_back('M');
}
for (int i = 0; i < ref_num; i++)
{
input_format.push_back('R');
}
afe_config_t *afe_config = afe_config_init(input_format.c_str(), models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
afe_config->aec_init = codec_->input_reference();
afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF;
afe_config->afe_perferred_core = 1;
afe_config->afe_perferred_priority = 1;
afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
afe_iface_ = esp_afe_handle_from_config(afe_config);
afe_data_ = afe_iface_->create_from_config(afe_config);
xTaskCreate([](void *arg)
{
auto this_ = (AfeWakeWord*)arg;
this_->AudioDetectionTask();
vTaskDelete(NULL); }, "audio_detection", 4096, this, 3, nullptr);
//新增一个任务
xTaskCreate([](void *arg)
{
auto this_ = (AfeWakeWord*)arg;
this_->CommandDetectionTask();
vTaskDelete(NULL); }, "command_detection", 4096, this, 5, &command_detection_task_);
}
TaskHandle_t command_detection_task_ = nullptr; //新增任务中的定义 我定义为了全局变量
esp_mn_iface_t *multinet; //全部定义的全局变量
model_iface_data_t *model_data;
第四步:实现新增任务的函数
void AfeWakeWord::CommandDetectionTask()
{
while (true)
{
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); //阻塞 收到通知后再跑
esp_mn_state_t mn_state;
esp_mn_results_t *mn_result;
if (model_data && res->data) //此处res使用的是原本唤醒AudioDetectionTask任务中的res,将其改为全局变量,记得一定要共用一个, afe_fetch_result_t *res; !
{
mn_state = multinet->detect(model_data, res->data);
//判断是否检测到唤醒词
if (mn_state == ESP_MN_STATE_DETECTING)
{
continue;
}
else if (mn_state == ESP_MN_STATE_DETECTED)
{
mn_result = multinet->get_results(model_data);
//此处id就是你添加唤醒词的id 注意 第一个是0 了,在此处自己可以写个回调函数任意处理 也可以控制其他设备 也可以当作唤醒词处理
if (mn_result != nullptr && mn_result->num > 0)
{
int command_id = mn_result->phrase_id[0];
if (command_id == 0)
{
Application::GetInstance().WakeWordInvoke("你好小诺");
}
else if (command_id == 1)
{
Application::GetInstance().WakeWordInvoke("小诺小诺");
}
else if (command_id == 2)
{
Application::GetInstance().WakeWordInvoke("山东华尔诺");
}
}
}
}
}
}
第五步:增加检测到有效音频数据通知处理任务
找到该函数
除了第四步注释中提到的将该函数中的res改为全局变量外,新增一个通知功能
// 通知
else if (res->data)
{
if (model_data)
{
xTaskNotifyGive(command_detection_task_);
}
}
第六步:补充头文件
这是我用到的头文件
至此 结束 编译烧录就可以了
大佬们有优化和创新 记得让我学习学习