智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

上一章:智能语音终端SDK快速上手说明 | 《无需从0开发 1天上手智能语音离在线方案》第三章>>>
下一章:智能语音终端开发板适配指南 | 《无需从0开发 1天上手智能语音离在线方案》第五章 >>>

1. 概述

本章介绍智能语音终端SDK的软件开发方法。
名词解释
下面介绍本文中涉及的一些专有名词:
• PCM:脉冲编码调制,这里专指未经过编码的语音数据。直接从麦克风采集出来的语音即为PCM数据。
• KWS:关键词识别,识别特定的几个词语。在我们方案中,该关键词为“宝拉宝拉”,该过程在设备端实现。
• ASR:语音识别,将声音转化为文字的过程。该过程在云端完成。
• NLP:自然语言处理,将文字转化成语义的过程。该过程在云端完成。
• TTS:文本转语音,将文字转换成语音数据。该过程在云端完成。
SDK目录介绍
下面是智能语音SDK的目录结构,表格中介绍了各个目录的功能。

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2. 核心组件介绍

本章介绍播放器组件、语音组件、云服务组件等核心组件。
• 播放器提供了语音播放功能,支持PCM、WAV、MP3等编码格式,可以实现内存音频、SD卡音频、在线音频等多种音频流播放。支持音频的播放、停止、暂停、继续、音量控制等多种音频控制命令。通过适配对应的声卡播放通路,可以灵活得实现多种播放场景。
• 语音组件提供了语音唤醒及麦克风音频采集功能,可以将唤醒事件、VAD事件、采集到的音频流等数据和事件实时传送给调用方,调用方结合云服务组件和播放器组件可以实现多种智能语音服务。
• 云服务组件提供了云端一体化的语音服务,封装了端侧和云侧的交互过程,开发者只需简单调用对应API、处理回调函数,即可方便的获得云端ASR/NLP识别结果和TTS合成服务。
• 配网服务组件提供了配网框架及多种配网模块,封装了统一的应用接口,开发者无需关心配网实现,即可通过统一接口获取多种配网服务。
• 闹铃服务组件提供了闹铃服务,不论系统处于运行状态、低功耗状态还是待机休眠状态,都可准时产生闹铃回调。
• 按键服务组件提供GPIO高低电平、上升/下降沿等多种类型的按键扫描功能。开发者设置对应的按键参数、回调函数,按键服务会自动扫描并触发对应按键事件。

2.1 播放器服务

2.1.1 功能介绍

播放器服务组件支持播放、停止、音量等通用控制功能外,还支持常用的最小音量控制、音乐打断恢复及音乐渐变切换功能。支持PCM、WAV、MP3等编码格式。

应用示例中实现了如下播放器服务功能:
• 通知音播放。
• 在线音频播放
• 云端合成TTS流播放
• SD卡音频播放

播放器服务组件的主要API如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2.1.2 代码示例

初始化

#include <aos/aos.h>
#include <media.h>

void app_player_init()
{
    /*创建任务*/
    utask_t *task_media = utask_new("task_media", 2 * 1024, QUEUE_MSG_COUNT, AOS_DEFAULT_APP_PRI);
    /*初始化*/
    ret = aui_player_init(task_media, media_evt);
}

事件回调函数
在aui_player_init初始化时传入了播放器服务回调函数,之后所有的播放器事件都会通过回调函数通知给用户。
为方便应用开发,播放器中支持了两种播放类型,通知类型和音乐类型。通知可以打断音乐的播放,通知结束后,可以设置音乐是否自动恢复播放。回调函数中有播放类型和事件ID,用户可根据需要在事件中增加应用功能。
播放器事件如下表:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

播放器类型如下表:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

#include <media.h>

static void media_evt(int type, aui_player_evtid_t evt_id)
{
    /*播放音乐的事件处理*/
    switch (evt_id) {
        case AUI_PLAYER_EVENT_START:
            break;
        case AUI_PLAYER_EVENT_ERROR:
            break;
        case AUI_PLAYER_EVENT_FINISH:
            break;
        default:
            break;
    }
}

播放网络音乐

aui_player_play(MEDIA_MUSIC, "http://test_url/AudioTest1.mp3/", 1);

播放SD卡中的音频

aui_player_play(MEDIA_MUSIC, "file:///fatfs/1.mp3", 1);

播放FIFO中音频

aui_player_play(MEDIA_MUSIC, "fifo://test1", 1);

调节音量

/*音量降低10*/
aui_player_vol_adjust(MEDIA_ALL, -10)
/*音量增加10*/
aui_player_vol_adjust(MEDIA_ALL, 10)
/*音量设置到50*/
aui_player_vol_set(MEDIA_ALL, 50);

2.2 语音服务

2.2.1 功能介绍

语音服务组件提供关键词识别和语音数据的处理控制。输入麦克风的语音数据经过回音消除、降噪和关键词识别处理后再输出到应用层使用。
语音服务组件的主要API如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2.2.2 代码示例

初始化
语音服务在一个独立的任务中运行。需要先创建一个任务,然后把任务句柄和回调函数传入初始化函数。

static int app_mic_init(int wwwv_enable)
{
    int ret;
    static voice_adpator_param_t voice_param;
    static mic_param_t param;

    /* 注册麦克风驱动 */
    voice_mic_register();

    /* 创建语音服务线程 */
    utask_t *task_mic = utask_new("task_mic", 3 * 1024, 20, AOS_DEFAULT_APP_PRI);
    ret               = aui_mic_start(task_mic, mic_evt_cb);

    memset(&param, 0, sizeof(param));

    param.channels          = 5; /* 麦克风通道数 */
    param.sample_bits       = 16; /* 比特数 */
    param.rate              = 16000; /* 采样率 */
    param.sentence_time_ms = 600;
    param.noack_time_ms    = 5000;
    param.max_time_ms      = 10000;
    param.nsmode           = 0; /* 无非线性处理 */
    param.aecmode          = 0; /* 无非线性处理 */
    param.vadmode          = 0; /* 使能VAD */
    param.vadswitch        = 1;
    param.vadfilter        = 2;
    param.vadkws_strategy  = 0;
    param.vadthresh_kws.vad_thresh_sp = -0.6;
    param.vadthresh_kws.vad_thresh_ep = -0.6;
    param.vadthresh_asr.vad_thresh_sp = -0.6;
    param.vadthresh_asr.vad_thresh_ep = -0.6;
    param.wwv_enable        = wwwv_enable; /* 二次唤醒使能配置 */

    voice_param.pcm         = "pcmC0";
    voice_param.cts_ms      = 20;
    voice_param.ipc_mode    = 1;
    voice_param.ai_param    = &param;
    voice_param.ai_param_len = sizeof(mic_param_t);
    param.ext_param1        = &voice_param;

    /* 配置语音服务参数 */
    aui_mic_set_param(&param);

    if (wwwv_enable) {
        /* 二次唤醒使能初始化 */
        app_aui_wwv_init();
    }

    return ret;
}

事件回调函数
在app_mic_init初始化时传入了语音服务回调函数,之后使用语音与设备交互时,所有的语音事件都会通过回调函数通知给用户。
语音事件如下表:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

下例程介绍了事件回调函数的典型实现:
• 检测到唤醒词后会首先收到唤醒事件MIC_EVENT_SESSION_START,调用函数aui_mic_control(MIC_CTRL_START_PCM)打开语音数据接收。
• 之后程序会持续接收到MIC_EVENT_PCM_DATA事件,获取到回音消除后的语音数据。将此音频通过云服务接口推送到云端,用于ASR/NLP识别。
• 当断句事件MIC_EVENT_SESSION_STOP发生时,调用函数aui_mic_control(MIC_CTRL_STOP_PCM)停止语音数据接收。

#include <yoc/mic.h>

static void mic_evt_cb(int source, mic_event_id_t evt_id, void *data, int size)
{
    switch (evt_id) {
        case MIC_EVENT_SESSION_START:
            /* 关键词唤醒,开始和云服务交互, 打开语音数据接收 */
            ret = app_aui_cloud_start(do_wwv);
            aui_mic_control(MIC_CTRL_START_PCM);
            break;
        case MIC_EVENT_PCM_DATA:
            /* 回音消除后的语音数据,推送到云端 */
            app_aui_cloud_push_audio(data, size);
            break;
        case MIC_EVENT_VAD:
            /* 检测到有效声音数据,可用于低功耗唤醒 */
            break;
        case MIC_EVENT_SESSION_STOP:
            /* 断句,停止和云服务交互,关闭语音数据接收 */
            app_aui_cloud_stop(1);
            aui_mic_control(MIC_CTRL_STOP_PCM);
            break;
        case MIC_EVENT_KWS_DATA:
            /* 接收到唤醒词数据 */
            break;
        default:;
    }
}

使能关键词检测
使用aui_mic_set_wake_enable开启和关闭关键词检测。例如使用按键开始语音交互时,就通过调用该函数关闭关键词检测。

if (1 == asr_en) {
    /* 使能关键词检测 */
    aui_mic_set_wake_enable(1);
} else {
    /* 关闭关键词检测 */
    aui_mic_set_wake_enable(0);
}

2.3 云服务

2.3.1 功能介绍

云服务组件提供应用与云端ASR/NLP/TTS服务交互的接口。调用对应服务API后,组件自动完成云端连接、鉴权、启动服务的过程,用户只需通过接口将需识别的音频或需合成的字符串传入,即可获得云端返回结果,设备端只需根据结果完成预定的应用行为。
云服务组件的主要API如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

API调用流程图

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2.3.2 代码示例

初始化
程序初始化时,需使用初始化函数aui_cloud_init初始化云服务,并设定对应参数。

#include <yoc/aui_cloud.h>

static aui_t        g_aui_handler;
int app_aui_nlp_init()
{
    /* 添加账号 */
    cJSON *js_account_info = NULL;
    cJSON_AddStringToObject(js_account_info, "device_uuid", device_uuid);
    cJSON_AddStringToObject(js_account_info, "asr_app_key", asr_app_key);
    cJSON_AddStringToObject(js_account_info, "asr_token", asr_token);
    cJSON_AddStringToObject(js_account_info, "asr_url", asr_url);
    cJSON_AddStringToObject(js_account_info, "tts_app_key", tts_app_key);
    cJSON_AddStringToObject(js_account_info, "tts_token", tts_token);
    cJSON_AddStringToObject(js_account_info, "tts_url", tts_url);
    
    aui_config_t cfg;
    cfg.per             = "aixia";
    cfg.vol             = 100;      /* 音量 0~100 */
    cfg.spd             = 0;        /* -500 ~ 500*/
    cfg.pit             = 0;        /* 音调*/
    cfg.fmt             = 2;        /* 编码格式,1:PCM 2:MP3 */
    cfg.srate           = 16000;    /* 采样率,16000 */
    cfg.tts_cache_path  = NULL;     /* TTS内部缓存路径,NULL:关闭缓存功能 */
    cfg.cloud_vad       = 1;        /* 云端VAD功能使能, 0:关闭;1:打开 */
    cfg.js_account      = s_account_info;
    cfg.nlp_cb          = aui_nlp_cb;
    g_aui_handler.config  = cfg;

    aui_asr_register_mit(&g_aui_handler);
    aui_tts_register_mit(&g_aui_handler);

    ret = aui_cloud_init(&g_aui_handler);
    aui_nlp_process_add(&g_aui_nlp_process, aui_nlp_proc_mit);
}

语音数据推送
在进行ASR识别时,需推送语音数据到云端。语音数据的推送流程主要在语音服务回调中处理。启动、推送、停止分别在语音服务事件MIC_EVENT_SESSION_START、MIC_EVENT_PCM_DATA、MIC_EVENT_SESSION_STOP中控制。具体代码已经在2.2.2 代码示例的“事件回调函数”一节有过介绍。

事件回调函数
云端下发的ASR和NLP结果通过aui_nlp_cb返回给用户。

#include <yoc/aui_coud.h>

/* 处理云端反馈的 ASR/NLP 数据,进行解析处理 */
static void aui_nlp_cb(const char *json_text)
{
    /* 处理的主入口, 具体处理见初始化注册的处理函数 */
    int ret = aui_nlp_process_run(&g_aui_nlp_process, json_text);
    switch (ret) {
        case AUI_CMD_PROC_ERROR:
            /* 没听清楚 */
            ...
            break;
        case AUI_CMD_PROC_NOMATCH:
            /* 不懂 */
            ...
            break;
        case AUI_CMD_PROC_MATCH_NOACTION:
            /* 不懂 */
            ...
            break;
        case AUI_CMD_PROC_NET_ABNORMAL:
            /* 网络问题 */
            ...
            break;
        default:;
    }
}

请求TTS服务
应用解析云端下发数据得到TTS文本之后,需要向云端请求TTS播放服务。代码段如下:

/* TTS回调函数 */
static void aui_tts_stat_cb(aui_tts_state_e stat)
{
    switch(stat) {
        case AUI_TTS_INIT:
            /* TTS初始化状态 */
            ...
            break;
        case AUI_TTS_PLAYING:
            /* TTS正在播放 */
            ...
            break;
        case AUI_TTS_FINISH:
            /* TTS播放完成 */
            ...
            break;
        case AUI_TTS_ERROR:
            /* TTS播放失败 */
            ...
            break;
    }
}

int app_aui_cloud_tts_run(const char *text, int wait_last)
{
    /* 注册回调函数 */
    aui_cloud_set_tts_status_listener(&g_aui_handler, aui_tts_stat_cb);
    /* 请求TTS播放服务,其中text是TTS文本 */
    return aui_cloud_req_tts(&g_aui_handler, text, NULL);
}

2.4 配网服务

配网服务由配网框架和配网模块组成,配网模块完成具体的配网功能,如一键配网、设备热点配网等,而配网框架封装了配网模块,为用户提供了统一的配网调用入口,简化了编程过程。

2.4.1 配网模块介绍

本服务提供设备的WiFi网络连接配置功能。支持两种配网方式,设备热点配网方式及阿里云生活物联网平台SDK配网方式。

设备热点网页配网
进入配网模式后,设备会辐射出一个WiFi热点。手机连接该热点后,会自动弹出配网页面,用户填写需连接的路由器SSID和密码信息。信息提交后,设备端会收到SSID和密码并连接路由器。如连接成功,设备会语音播报并广播配网成功信息,手机端监测到成功信息并弹出提示。如超时未成功,设备会语音播报配网超时。

生活物联网平台SDK配网
生活物联网平台SDK为阿里云生活物联网平台提供的设备端SDK,包含WiFi配网、云端连接及设备控制功能。配合生活物联网平台手机端SDK,可以形成多样化的网络解决方案。
生活物联网平台SDK的WiFi配网方式支持一键配网和设备热点配网两种方式,可互为补充,其操作特点如下:
• 一键配网:手机无需切换路由,用户在APP上输入所连路由器SSID和密码,手机进入一键配网模式后,会不停发送包含SSID和密码的802.11射频加密报文。设备截获报文并解密后,即可连接路由器。此配网过程十分方便迅速,但可能存在兼容性问题。
• 设备热点配网:与设备热点网页配网原理类似,不过手机端需使用APP发送SSID和密码。设备接收到SSID和密码后自动连接路由器。此配网方式兼容性好,适合作为备用方案。

2.4.2 配网框架介绍

配网框架提供了配网模块的注册、配网的启动、停止等接口,为底层不同的配网模块提供了统一的接口函数。
在配网流程启动前,我们需要先注册配网方法,该动作接口需要适配层实现。启动完成后应用层将通过调用配网框架提供接口wifi_prov_start来进入配网状态。配网状态退出有三种方式,超时自动退出,收到配网结果自动退出,调用接口 wifi_prov_stop 主动退出。框架接口wifi_prov_start,支持多个配网方法同时启动,如果几个方法没有互斥。退出配网状态时,所有启动的配网将一起退出。

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

配网流程
配网服务组件的主要API如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2.4.3 代码示例

初始化
程序初始化时,首先需要注册所需配网模块,可同时注册多种:

#include <softap_prov.h>
#include <wifi_provisioning.h>

/*注册设备热点网页配网服务,参数为SSID的前缀*/
wifi_prov_softap_register("YoC");

/*注册生活物联网平台配网服务*/
wifi_prov_sl_register();

启动配网
需要配网时,启动对应的配网模块,进入配网流程。

#include <softap_prov.h>
#include <wifi_provisioning.h>

/*启动配网服务,超时时间120秒*/
wifi_prov_start(wifi_prov_get_method_id("softap"), wifi_pair_callback, 120);

事件回调函数
配网成功或失败都会调用该函数,函数的参数中event变量可以确认结果,如果由多种配网同时启动可以从method_id确认哪种配网,配网参数从result返回,可使用该参数去连接网络。

#include <wifi_provisioning.h>

static void wifi_pair_callback(uint32_t method_id, wifi_prov_event_t event, wifi_prov_result_t *result)
{
    if (event == WIFI_PROV_EVENT_TIMEOUT) {
        /*配网超时*/
        LOGD(TAG, "wifi pair timeout...");
    } else if (event == WIFI_RPOV_EVENT_GOT_RESULT) {
        /*配网成功,获取配网参数*/
        LOGD(TAG, "wifi pair got passwd ssid=%s password=%s...", result->ssid, result->password);
    }
}

停止配网
调用该函数停止配网流程,若启动多种配网也会全部停止,退出配网状态。

#include <wifi_provisioning.h>

/*停止配网*/
wifi_prov_stop();

2.5 闹铃

2.5.1 功能介绍

闹铃服务组件提供设备闹铃提醒的功能。用户设置对应闹铃模式及回调函数后,闹铃到时,通过回调函数向用户抛出对应闹铃事件。

**闹铃服务具有如下特点:
**• 可设置“仅响一次”、“每天”、“每周”或“工作日”模式
• 支持最多5个闹铃
• 支持待机状态下唤醒
• 秒级精度
闹铃服务组件的主要API如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2.5.2 代码示例

初始化
系统启动时,调用函数clock_alarm_init初始化闹铃服务,初始化函数会从系统Flash中获取数据,更新闹铃信息和状态。

#include <clock_alarm.h>
#include <rtc_alarm.h>

void main()
{
    ...
    clock_alarm_init(app_clock_alarm_cb);
    ...
}

事件回调函数
闹铃到时,系统会调用app_clock_alarm_cb函数,用户可在该函数中处理对应闹铃事件。若设置了多个闹铃,参数clock_id来指示哪个闹铃。

#include <cloc_alarm.h>

/* 处理闹铃到时提醒 */
static void app_clock_alarm_cb(uint8_t clock_id)
{
    LOGI(TAG, "clock_id %d alarm cb handle", clock_id);

    char url[] = "http://www.test.com/test.mp3"; //url示例
    /* 播放闹铃音乐 */
    mplayer_start(SOURCE_CLOUD, MEDIA_SYSTEM, url, 0, 1);
}

闹铃设置
用户新增一个闹铃,需要设置闹铃的模式(即一次、工作日、每周、每天),输入具体的闹铃时分秒信息。

clock_alarm_config_t cli_time;

cli_time.period = CLOCK_ALARM_PERIOD_WORKDAY;
cli_time.hour = 7;
cli_time.min = 30;
cli_time.sec = 0;

/* 新增闹钟 */
id = clock_alarm_set(0, &cli_time);

/* 修改闹钟,clock_id是被修改闹钟id号 */
id = clock_alarm_set(clock_id, &cli_time);

/* 删除闹钟, clock_id是被删除闹钟id号 */
clock_alarm_set(clock_id, NULL);

2.6 按键服务

2.6.1 功能介绍

按键服务组件提供多种类型的按键扫描功能。用户可添加对应的按键类型、引脚号、触发阈值,启动后会周期性扫描按键引脚,当键值满足触发条件后,通过回调函数向用户抛出对应按键事件。

按键服务具有如下特点:
• 最多支持10个按键
• 支持GPIO高低电平按键
• 支持GPIO电平变化检测
• 支持单一按键和组合按键(2个键组合),支持短按、长按

按键服务组件的主要API如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

2.6.2 代码示例

初始化
按键服务的初始化和配置需要四个步骤:
• 定义单一按键表和组合按键表
• 创建task和一个消息队列,用于接收按键消息,并将用户处理函数注册到task中
• 初始化按键服务和按键表
• 根据需要配置按键参数(例如超时时间等)

按键表定义

/* 单一按键表 */
const static button_config_t button_table[] = {
    {APP_KEY_MUTE,    (PRESS_UP_FLAG | PRESS_LONG_DOWN_FLAG), button_evt, NULL, BUTTON_TYPE_GPIO,  "mute"},
    {APP_KEY_VOL_INC, (PRESS_UP_FLAG | PRESS_LONG_DOWN_FLAG), button_evt, NULL, BUTTON_TYPE_GPIO,  "inc"},
    {APP_KEY_VOL_DEC, (PRESS_UP_FLAG | PRESS_LONG_DOWN_FLAG), button_evt, NULL, BUTTON_TYPE_GPIO,  "dec"},
    {APP_KEY_STANDBY, (PRESS_UP_FLAG | PRESS_LONG_DOWN_FLAG), button_evt, NULL, BUTTON_TYPE_GPIO,  "standby"},
    {0, 0, NULL, NULL},
};

/* 组合按键表 */
const static button_combinations_t bc_table[] = {
    {
        .pin_name[0] = "mute",
        .pin_name[1] = "inc",
        .evt_flag = PRESS_LONG_DOWN_FLAG,
        .pin_sum = 2,
        .tmout = 500,
        .cb = bc_evt,
        .priv = NULL,
        .name = "mute&inc_long"
    },
    {
        .pin_name[0] = "mute",
        .pin_name[1] = "dec",
        .evt_flag = PRESS_LONG_DOWN_FLAG,
        .pin_sum = 2,
        .tmout = 500,
        .cb = bc_evt,
        .priv = NULL,
        .name = "mute&dec_long"
    },
    ...
}

创建任务和消息队列

aos_task_t task;
static aos_queue_t s_queue;
static uint8_t s_q_buffer[sizeof(evt_data_t) * MESSAGE_NUM];

aos_queue_new(&s_queue, s_q_buffer, MESSAGE_NUM * sizeof(evt_data_t),sizeof(evt_data_t));
aos_task_new_ext(&task, "b-press", button_task_thread, NULL, 4096, AOS_DEFAULT_APP_PRI + 4);

初始化按键服务,并配置按键参数

button_task();
button_srv_init();
button_init(button_table);
button_param_t pb;
button_param_cur("mute", &pb);
pb.ld_tmout = 2000;
button_param_set("mute", &pb);
button_param_set("inc", &pb);
button_param_set("dec", &pb);
button_combination_init(bc_table);

事件回调函数

#include <yoc/adc_key_srv.h>

static void button_task_thread(void *arg)
{
    evt_data_t data;
    unsigned int len;

    while (1) {
        aos_queue_recv(&s_queue, AOS_WAIT_FOREVER, &data, &len);

        if (strcmp(data.name, "mute") == 0) {
            if (data.event_id == BUTTON_PRESS_LONG_DOWN) {
                /* mute长按 */
                ...
            } else if (data.event_id == BUTTON_PRESS_UP) {
                /* mute短按 */
                ...
            }
        } else if (strcmp(data.name, "inc") == 0) {
            /* inc按键 */
            ...
        } else if (strcmp(data.name, "dec") == 0) {
            /* dec按键 */
            ...
        } else if (strcmp(data.name, "standby") == 0) {
            /* standby按键 */
            ...
        } else if (data.event_id == BUTTON_COMBINATION) {
            /* 组合按键 */
            ...
            if (strcmp(data.name, "mute&inc_long") == 0) {
                /* mute和inc组合长按 */
                ...
            } else if (strcmp(data.name, "mute&dec_long") == 0) {
                /* mute和dec组合长按 */
                ...
        }
    }
}

3. 应用示例讲解

上文中已经介绍了播放器、语音、云服务三个核心组件的功能和使用方法。三者间关系如下图,应用通过语音服务获取到音频输入,通过云服务推送给云端进行ASR/NLP识别,再将结果进行TTS合成,生成的音频通过播放器播放出来。

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

3.1 方案介绍

智能语音应用包含网络功能,音频播放服务、语音服务(关键词识别和数据交互)、云服务等。整个语音交互流程如下图。

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章
语音交互流程图

通用交互流程
• 使用者说出“宝拉宝拉,今天天气怎么样”时,语音服务识别出“宝拉宝拉”这个关键词,产生唤醒事件。
• 语音唤醒事件中控制开始录音。
• 使用者继续说“今天天气怎么样”,语音数据回调中推送录音音频给云端。
• 使用者说完,产生语音断句事件,事件中停止录音和云端推送。
• 云端依次执行 ARS->NLP->TTS 三个服务:先将语音数据转化成文字“今天天气怎么样”,然后通过NLP算法,理解语义,得知是天气查询,最后通过云端技能接口获得有关的天气信息,再将天气信息的文字转成语音音频,推送给设备端。
• 设备端收到语音音频后调用播放器播放。

流程差异
当然并不是所有的语音交互都遵循这个的流程,例如调整音量等命令类的语音交互,当使用者说出“声音小一点”时,设备端只需要获取到云端下发的NLP结果,判断为设备控制命令 ,就可以直接控制设备行为,而无需后续TTS流程。
同时还需认识到,使用不同的云端服务,调用的过程也会有区别。例如:
• 有些云服务器在进行ASR/NLP识别过程中不会单独下发ASR结果,而是直接下发最终的NLP结果。
• 不同云端NLP结果的封装方式差异很大。
• 有些云端不需要设备端参与TTS过程,会直接下发语音数据。

3.2 入口函数

应用的入口函数文件如下:

app/src/app_main.c

入口函数主要有,板级、系统级、音频驱动、网络、播放器、语音服务、云服务等模块的初始化。初始化完成之后,不同的服务和任务就在各自的线程中独立运行。

void main()
{
    board_base_init();
    yoc_base_init();

    LOGI(TAG, "Build:%s,%s",__DATE__, __TIME__);

    /* 系统事件处理 */
    sys_event_init();
    app_sys_init();

    /* 初始化LED灯 */
    app_status_init();

    /* 音频数据采集 */
    board_audio_init();

    /* 初始化PWM LED灯 */
#if defined(APP_PWM_EN) && APP_PWM_EN
    app_pwm_led_init();
#endif

    /* 低功耗初始化 */
    app_lpm_init();

    /* 启动播放器 */
    mplayer_init(4 * 1024, media_evt, 20);

    /* 配置EQ */
    aui_player_sona_config(sona_aef_config, sona_aef_config_len);

    /* 启动麦克风服务 */
    app_mic_init(1);

    /* 开启功放 */
    app_speaker_mute(0);

    /* 网络初始化 */
    wifi_mode_e mode = app_network_init();

    if (mode != MODE_WIFI_TEST) {
        if (mode != MODE_WIFI_PAIRING &&
            app_sys_get_boot_reason() != BOOT_REASON_WIFI_CONFIG &&
            app_sys_get_boot_reason() != BOOT_REASON_WAKE_STANDBY) {
                local_audio_play(LOCAL_AUDIO_STARTING);
            }
#if defined(APP_FOTA_EN) && APP_FOTA_EN
        /* FOTA升级初始化 */
        app_fota_init();
#endif

        if (g_fct_mode) {
            /* 产测初始化 */
            fct_case_init();
        }

        /* 交互系统初始化 */
        app_aui_nlp_init();
        app_text_cmd_init();
    }

    /* 按键初始化 */
    app_button_init();

    /* LED状态初始化 */
    app_set_led_state(LED_TURN_OFF);

    /* 命令行测试命令 */
    cli_reg_cmds();

    return;
}

3.3 事件处理机制

系统中驻留一独立任务来处理事件,用户可通过订阅接口来获取系统事件。下面通过网络模块的实现代码来讲解该机制的使用方法。
代码路径

app/src/app_net.c

事件回调函数

static void user_local_event_cb(uint32_t event_id, const void *param, void *context)
{
    if ((wifi_is_pairing() == 0) && wifi_network_inited()) {
        network_normal_handle(event_id, param);
        network_reset_handle(event_id);
    } else {
        LOGE(TAG, "Critical network status callback %d", event_id);
    }
}

订阅系统事件
通过函数app_net_init进行网络的初始化并订阅网络事件,注册用户回调函数user_local_event_cb。在程序中,当网络事件发生时,事件处理机制会调用user_local_event_cb函数进行网络事件的处理。
网络事件如下:

智能语音应用开发指南 | 《无需从0开发 1天上手智能语音离在线方案》第四章

#include <aos/aos.h>
#include <yoc/netmgr.h>
#include <yoc/eventid.h>
#include <devices/wifi.h>

static wifi_mode_e app_net_init(void)
{
    char ssid[32 + 1] = {0};
    int ssid_len = sizeof(ssid);
    char psk[64 + 1] = {0};
    int psk_len = sizeof(psk);

    /* 系统事件订阅 */
    event_subscribe(EVENT_NETMGR_GOT_IP, user_local_event_cb, NULL);
    event_subscribe(EVENT_NETMGR_NET_DISCON, user_local_event_cb, NULL);

    /* 使用系统事件的定时器 */
    event_subscribe(EVENT_NTP_RETRY_TIMER, user_local_event_cb, NULL);
    event_subscribe(EVENT_NET_CHECK_TIMER, user_local_event_cb, NULL);
    event_subscribe(EVENT_NET_LPM_RECONNECT, user_local_event_cb, NULL);

    aos_kv_get("wifi_ssid", ssid, &ssid_len);
    aos_kv_get("wifi_psk", psk, &psk_len);
    if (strlen(ssid) == 0) {
        wifi_pair_start();
        return MODE_WIFI_PAIRING;
    } else {
        wifi_network_init(ssid, psk);
    }
    return MODE_WIFI_NORMAL;
}

自定义事件
除了系统事件以外,事件机制也允许用户自定义事件,我们以EVENT_NTP_RETRY_TIMER为例。
app_main.h中统一管理所有用户自定义事件

#define EVENT_NTP_RETRY_TIMER       (EVENT_USER + 1)
#define EVENT_NET_CHECK_TIMER       (EVENT_USER + 2)
#define EVENT_NET_NTP_SUCCESS       (EVENT_USER + 3)
#define EVENT_NET_LPM_RECONNECT     (EVENT_USER + 4)

下面的代码是网络事件处理函数,回调中获取到EVENT_NETMGR_GOT_IP事件后,发送EVENT_NTP_RETRY_TIMER消息启动NTP对时,然后在回调的EVENT_NTP_RETRY_TIMER事件分支中调用ntp_sync_time进行对时,如果对时失败,调用event_publish_delay发送EVENT_NTP_RETRY_TIMER延时消息,一定时间后重新对时。

#include <yoc/netmgr.h>
#include <yoc/eventid.h>

static void network_normal_handle(uint32_t event_id, const void *param)
{
    switch (event_id) {
    case EVENT_NETMGR_GOT_IP: {
        /* 启动NTP对时 */
        event_publish(EVENT_NTP_RETRY_TIMER, NULL);
    } break;

    case EVENT_NETMGR_NET_DISCON: {
        LOGD(TAG, "Net down");
        /* 不主动语音提示异常,等有交互再提示 */
        internet_set_connected(0);
    } break;

    case EVENT_NTP_RETRY_TIMER:
        if (ntp_sync_time(NULL) == 0) {
#if CONFIG_RTC_EN
            /* 网络对时成功,同步到RTC中 */
            rtc_from_system();
#endif
            if (wifi_internet_is_connected() == 0){
                /* 同步到时间,确认网络成功,提示音和升级只在第一次启动 */
                internet_set_connected(1);

                app_status_update();
                local_audio_play(LOCAL_AUDIO_NET_SUCC);
                event_publish(EVENT_NET_NTP_SUCCESS, NULL);
            }
        } else {
            /* 同步时间失败重试 */
            event_publish_delay(EVENT_NTP_RETRY_TIMER, NULL, 6000);
        }
        break;
    default:
        break;
    }
}
上一篇:智能语音终端SDK快速上手说明 | 《无需从0开发 1天上手智能语音离在线方案》第三章


下一篇:智能语音终端开发板适配指南 | 《无需从0开发 1天上手智能语音离在线方案》第五章