1. Yimo
1.1. 1. 简介
Yimo 工程的主要目的是
- 实现类似 iOS 和 Android 系统中应用开发所需要的基础模块,比如 runloop,timer,av,ui 等
- 快速接入外部SDK,比如 tvs,avs 等,实现基本但功能完整的业务逻辑
- 接入内部实现的前沿框架,提供可见的产品实现,供外部参考
Yimo 中虽然包含具体的产品实现,但是不会有产品经理介入,所以产品实现应该保持简洁,避免不必要的复杂业务逻辑。
Yimo 输出
- libyimo.so:包含所有的基础模块,未来模块复杂之后可能需要拆分成多个 so 文件
- targets:具体的产品实现,比如 tvs,avs,可以配置 CMAKE_OPTIONS 选择要编译的 target,支持同时编译多个target;最终的可执行文件名包含
yimo-
前缀
1.2. 2. cmake 选项
主要提供两种 cmake 选项,target和feature
- target 类型:YIMO_TARGET_ 前缀,选择要编译的 target
- feature 类型:YIMO_ENABLED_ 前缀,是否打开某个特性或者外部库
现有的 cmake 选项
选项 | 说明 |
---|---|
YIMO_TARGET_DEMO | 是否编译 demo target |
YIMO_TARGET_TVS | 是否编译 tvs target |
YIMO_TARGET_AVS | 是否编译 avs target |
YIMO_ENABLED_LIB_INPUT_EVENT | 是否使用 input-event 库 |
YIMO_ENABLED_LIB_DISPLAY | 是否使用 display 库 |
另外一个选项 YIMO_HARDWARE
指定硬件抽象接口实现,会根据该选项编译 src/hw
中对应的子目录;未定义或子目录不存在时会返回错误中止编译。
目前工程中只有根目录一个CMakeList.txt文件。
1.3. 3. 模块说明
1.3.1. 3.1 target
src/targets
目录包含所有的 target,其中 main.cpp
作为程序入口,最主要的工作是为 Context
指定 delegate 并启动;其次是解析所需的命令行参数。
ContextDelegate
是业务逻辑的主要入口,继承自 ContextDelegateInterface
,包含以下虚函数
virtual void contextWillBegin();
virtual void contextDidEnd();
virtual bool acceptEvent(Event *event);
virtual void handleEvent(Event *event);
在 Context
开始,结束和收到事件后执行必要的操作。
1.3.2. 3.2 hardware
src/hw
目录中包含所有支持的硬件平台。基础模块比如av可能有一些接口需要不同的硬件平台实现,可以使用 YIMO_HARDWARE
cmake 选项指定对应的平台。
1.3.3. 3.3 基础模块
几个基础模块core,av,net,application相对比较独立。
3.3.1 core
提供应用开发核心的功能,比如 runloop,timer,event抽象,buffer 等
- Object:几乎所有类的基类。构造时保存一个 weak_ptr 引用,执行异步操作时 AsyncBlock 只 capture 这个弱引用,避免循环引用
- Runloop:基于
libuv
,提供事件循环,是应用开发事件驱动模型的基础 - Timer:基于
libuv/uv_timer_t
,实现单次或重复的定时器 - Context:封装 Runloop,管理 delegate,分发事件
- Task:类似 JavaScript 中的 Promise,支持异步操作链式执行
- Event:语音,按键事件基类,处理时使用
dynamic_cast
判断具体的类型 - Buffer:内存 buffer
3.3.2 av
audio & video模块,目前只有 audio。
- Audio:提供音频的基本描述,encoding,format,packet等;抽象录音接口,需要硬件平台实现
- MediaCenter:管理播放器,音量通道等
3.3.3 net
网络相关模块,目前只有 http 接口,可能还需要有 websocket,mqtt 等。
- Http:发出 http 请求,回调在 Runloop 主线程执行
3.3.4 ui
Stupid:临时实现,屏幕居中显示云端或本地图片
3.3.5 application
应用模块,与业务无关。
- Directive:指令是 app 执行的最小单位,支持单个或者 Group 指令,支持暂停,取消等操作
- SimpleApp:一个简单的 app,一般是单例,只能按顺序执行指令
1.4. 4. 异步开发
Yimo 中的异步实现主要依赖一下几个模块
- core/Runloop:提供事件循环,
async
和background
等函数,统一的AsyncBlock
lambda 原型定义 - core/Object:使用 make_shared 构造对象,返回 shared_ptr 前设置 weak_ptr,AsyncBlock 中引用 weak_ptr,避免循环引用
- core/Task:类似 JavaScript 中的 Promise,支持链式的异步操作,和 Promise 一样没有 cancel 方法。
net/Http
模块提供Task
接口,也可以使用Task<T>::create
函数构造Task
实例 - application/Directive:抽象的语音指令,支持单条或者组合指令,通常继承
Directive
实现自定义指令,比如 tts 和音乐播放
1.4.1. 4.1 Runloop
core/Runloop
基于 libuv
,包含一个主线程,遇到事件时执行相应的函数,没有事件时等待;还有数个后台线程,调用background
指定的 AsyncBlock 会分配到这些后台线程执行,执行完毕后回调依旧在主线程调用。
core/Runloop
目前封装的功能见下表
libuv | core/Runloop |
---|---|
uv_async_t | async 函数 |
uv_work_t | background 函数 |
uv_poll_t | watch 函数 |
uv_timer_t | after 函数和 Timer 类 |
1.4.2. 4.2 返回 shared_ptr
模块的 public 接口不应该返回裸指针,要返回的类型应该继承 Object
,使用 NEW_OBJECT
宏构造,返回 shared_ptr 类型。单例接口(shared()
)一般返回单例的引用。
1.4.3. 4.3 示例
4.3.1 最简单的异步操作
// 获取 this 弱引用
auto weak = this->weakRef<ClassOfThis>();
Context::shared().async([=] {
// 检查弱引用是否有效,如果无效说明 this 已经被析构
YIMO_CHECK_WEAK_REF(strong, weak);
// 执行操作
strong->doSomeThing();
});
4.3.2 从云端下载图片并显示
// 获取 this 弱引用
auto weak = this->weakRef<ClassOfThis>();
// 下载图片
Http::request("http://xxx.com/yyy.img", "GET", 0, [=](Http::Handle *handle) {
// 检查弱引用是否有效,如果无效说明 this 已经被析构
YIMO_CHECK_WEAK_REF(strong, weak);
auto resp = handle->response;
// 绘制图片
strong->drawImage(resp.body, resp.contentLength);
});
4.3.3 请求云端大段的数据,处理后把结果保存到本地
// 获取 this 弱引用
auto weak = this->weakRef<ClassOfThis>();
// 请求数据
Http::request("http://xxx.com/yyy.json", "GET", 0, [=](Http::Handle *handle) {
// 检查弱引用是否有效,如果无效说明 this 已经被析构
YIMO_CHECK_WEAK_REF(strong, weak);
auto result = process(handle->body, handle->contentLength);
Context::shared().background([=] {
save(result, "path/to/file");
}, [=] (int error) {
// 每个 AsyncBlock 里都要检查弱引用
YIMO_CHECK_WEAK_REF(strong, weak);
strong->onSave();
});
});
4.3.4 使用 task 链式执行
// 获取 this 弱引用
auto weak = this->weakRef<ClassOfThis>();
// 请求数据
auto reqTask = Http::task("http://xxx.com/yyy.json")
auto processTask = reqTask
->then<int>([=](Http::Handle *handle) -> int {
// 检查弱引用是否有效,如果无效说明 this 已经被析构
YIMO_CHECK_WEAK_REF(strong, weak);
int result = process(handle->body, handle->contentLength);
strong->onResult(result);
// 还不支持 void 类型 Task
return result;
})
->fail(0, [=](int error) {
// 检查弱引用是否有效,如果无效说明 this 已经被析构
YIMO_CHECK_WEAK_REF(strong, weak);
strong->onError(error);
});
1.5. 5. TVS
tvs-sdk
主要提供唤醒识别,ASR,NLP 和 TTS,功能等同于 rokid 的turen
模块,所以 tvs
target 主要的工作是(NLP 在 tvs-sdk
中被称为 semantic
,为了方便理解和统一,本文档中还是使用 NLP
这个名字)
- 录音(signed 16 bit,16k,单通道),输入到
tvs-sdk
- 按流程切换状态获取 ASR 和 NLP
- 解析和执行 NLP
1.5.1. 5.1 录音
src/hw/k18
中实现了 av/Audio.h
中定义的 AudioRecorderInterface
,支持录制原始数据和经过 AEC 处理过的数据(使用 YIMO_K18_RECORD_AEC
宏切换)。
tvs/Tvs
作为 tvs-sdk
的封装,读取录音并输入到tvs-sdk
。
1.5.2. 5.2 状态管理
状态管理也在 tvs/Tvs
,所有的状态如下表
状态 | 说明 |
---|---|
NONE | 初始状态,只会在初始化阶段使用该状态,之后不会再出现 |
REQUEST_WAKEUP_DETECT | 请求激活状态,tvs-sdk 返回请求成功后才可以进入等待激活状态 |
WAKEUP_DETECTING | 等待激活状态,静置设备时应该在此状态,此时录音数据输入到 tvs-sdk 唤醒模块 |
REQUEST_ASR | 激活后请求开始ASR识别,tvs-sdk 返回请求成功后才可以进入 ASR 识别状态 |
ASR_RECOGNING | ASR 识别状态,此时录音数据输入到 tvs-sdk asr模块 |
REQUEST_SEMANTIC_PARSE | 请求 NLP 状态,输入为 ASR_RECOGNING 状态结束时返回的 ASR |
REQUEST_SEMANTIC_PARSE 请求返回后会重新进入 REQUEST_WAKEUP_DETECT 状态;任何请求发生错误后也会重新进入 REQUEST_WAKEUP_DETECT 状态。
src/Tvs
还封装了 TTS 请求,但是播放 TTS 时应该使用 tvs/SpeechDirective
。
1.5.3. 5.3 执行 NLP
tvs-sdk
的 NLP 也有 rokid NLP 中 appId,intent,slots 等内容,只是名称或结构有差异;同时 data
节点比较规整,与 rokid cloud app 的 action 比较相似。
解析和执行 NLP 主要在 tvs/Executor
中完成,播放音乐的 NLP 结构见下图
tvs/Executor
中普通应用根据 data/controlInfo/type(AUDIO
/TEXT
) 将 data/listItems 列表转换为音乐(MediaCenter::ListPlayingHandle)或 speech(SpeechDirective)指令;部分应用做了特殊处理,比如音乐除了可以播放,还要支持”上一首“和”下一首“控制指令。
音乐除了不只解析资源的 URL,同时还有专辑,歌手,封面等信息,方便进行功能扩展。
解析出指令(application/Directive
)列表后会交给一个单例的 application/SimpleApp
执行。