为什么有这个想法,因为自己有过体验,当初做的第一个语音增强的demo,傻乎乎的自己实现特征提取,自己实现网络前向,于是就需要将kaldi的网络参数转写成方便自己程序读取的格式,还需要不断对比自己实现的特征结果和HTK的结果是否一致,这期间花费的时间个人觉得已经远远的超出做demo本身的意义,最主要的是,由于当初实现的代码仅仅是针对当初的需求,而一旦后期的特征配置或者网络结构发生变化,之前的工作就要重复一次,如此低效率的事情我个人是不想重复做的。
所以,最简单的方法就是,将计算逻辑交给kaldi完成,自己只需要完成控制逻辑上的编码。本身使用NDK+JNI进行开发并不是很难,无非是将kaldi的依赖和自身编译成库,和自己编写的JNI接口链接,通过Android Studio支持的cmake或者ndk构建最终的动态库就行了,但是,由于网上资料过少,链接错误又比较难查,所以还是耗费了不少时间的。
我的需求是提取nnet1和feat两块,所以不需要openfst以及其他无关的代码,最终提取出的工作目录如下(实际上还可以进一步精简,test文件一律删除)
1 | base cudamatrix feat hmm itf matrix nnet thread tree util |
所有步骤都可以从以下链接中获取
我踩过的坑如下:
配置本地编译环境
这一步之前编译OpenBlas的时候也用过,使用ndk自带的make_standalone_toolchain.py
脚本配置编译环境,安装完毕之后,将安装目录下的bin文件夹导入环境变量。脚本比较重要的参数是--api
和--stl
。其中--stl
配置NDK的运行时(gnustl/libc++/stlport),--api
配置SDK的版本,这两个配置选项要和后续的Application.mk保持一致的,分别对应APP_STL
和APP_PLATFORM
。--arm
配置编译平台,安卓手机对应arm
或者arm64
,对应Application.mk中APP_ABI
的armeabi/armeabi-v7a
和arm86-v8a
。另外,切记,openblas和kaldi在同意一个toolchain下完成编译。
编译clapack
这个也很简单,github上有人已经构建好了工程,简单修改Android.mk执行ndk-build就行。
编译openblas
openblas编译基本不会出现问题,参照上面给出的链接即可。将clapack生成的blas,lapack,clapack,f2c四个库拷贝到openblas安装目录下的lib中,kaldi会自动配置到kaldi.mk中作为链接库。
编译kaldi
由于修改了原先的工程目录,所以需要修改一下Makefile和configure文件
- 去除configure文件中关于openfst的检查脚本
- 修改Makefile中的SUBDIR(控制编译那些目录),以及各子目录中的Makefile(ADDLIBS是本目录的依赖库,OBJFILES是要打包到静态库中的目标文件,不需要生成测试文件就将TESTFILES注释即可)
- 注释android_openblas.mk中openfst的宏定义,将宏
ANDROIDINC
修正为ANDROIDINCDIR
(脚本的bug,因为configure中定义的是ANDROIDINCDIR
),注意一下CXXFLAGS
的参数,后面链接的时候要用到。 - kaldi默认在编译android平台只能生成静态库
AS的配置
上述过程倒是很少出现问题,在AS中用cmake或者ndk生成动态库是主要的头疼点(一系列让人摸不着头脑的链接错误)。首先说明kaldi的编译和链接参数如下(来自android_openblas.mk):1
2
3
4
5-DKALDI_DOUBLEPRECISION=0 # BaseFloat为单精度
-DHAVE_CXXABI_H
-DHAVE_OPENBLAS # 在cblas_wrapper.h中用到,表示blas用openblas实现
-DANDROID_BUILD -ftree-vectorize
-mfloat-abi=hard -mfpu=neon -mhard-float -D_NDK_MATH_NO_SOFTFP=1 # openblas用到
以及1
2-Wl,--no-warn-mismatch
-lm_hard # openblas用到
以上参数需要在Android.mk或者CmakeLists.txt中等价的实现,cmake我没有成功过,Android.mk目前配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27LOCAL_PATH := $(call my-dir)
# 预构建静态库
include $(CLEAR_VARS)
LOCAL_MODULE := kaldi-prebuild
LOCAL_SRC_FILES := libkaldi.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := kaldi
LOCAL_SRC_FILES := impl.cpp
# 编译选项
LOCAL_CFLAGS += -DKALDI_DOUBLEPRECISION=0 -DHAVE_CXXABI_H \
-DHAVE_OPENBLAS -DANDROID_BUILD -ftree-vectorize \
-mfloat-abi=hard -mfpu=neon -mhard-float -D_NDK_MATH_NO_SOFTFP=1
# 链接选项
LOCAL_LDFLAGS += -Wl,--no-warn-mismatch -lm_hard
# 链接静态库,为什么只有一个?我把各个静态库压成一个了
LOCAL_STATIC_LIBRARIES += kaldi-prebuild
# 定义头文件路径
LOCAL_C_INCLUDES += $(LOCAL_PATH)/blas $(LOCAL_PATH)
# 动态链接log库
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
对应的Application.mk如下,之前说明过,配置要和toolchain保持一致1
2
3
4APP_ABI := armeabi-v7a # 用到了openblas
APP_STL := c++_static # libc++
APP_CPPFLAGS := -fexceptions # 打开异常开关
APP_PLATFORM := android-24 # SDK-24
这期间遇到的问题如下:
cmake的VFP问题
使用链接openblas如果不加-mhard-float -D_NDK_MATH_NO_SOFTFP=1 -lm_hard
这些选项的话,会出现*.a use VFP arguments, but output not
的链接错误,但是,我在CmakeLists.txt中的add_definitions
和gradle中的cppFlags
都尝试过添加这些选项,均没有解决该问题,只能转战ndk+Android.mk了。
f2c_的未定义问题
这个推测应该是和静态库的链接顺序有关系,Android.mk要链接静态库,须先预构建一下(我暂时不知道其他链接方法),然后将预构建好的模块进行连接(加到LOCAL_STATIC_LIBRARIES
参数之后),有一种说法是越基本的库放的越靠后,我在kaldi-base.a kaldi-matrix.a
和五个blas相关的库上做过各种顺序的尝试,但是始终没能解决该问题(要么是f2c
的为定义,要么是openblas
的未定义),最终用粗暴的方法,将这几个库打包成一个静态库解决该问题的(尽量保证openblas和clapack库是同一个toolchain构建的)
ndk命名空间错误
这个错误是后来做了若干修正之后解决的,现在回想,可能的原因是Application.mk中的APP_PLATFORM
和APP_STL
和toolchain配置的api/stl
不匹配。
到这里,在jni文件夹外边使用ndk-build
已经可以直接生成静态库了,但是,如果在AS中build的话,还是会出现rand_r, rand, posix_memalign
这些函数的未定义,目前这些问题还没有解决,我通过gradle配置ndk的构建方法替换AS自身的逻辑来跳过这个错误,在gradle中android{}
添加配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15sourceSets.main {
jni.srcDirs = [] # 禁用AS默认的jni目录
}
task ndkBuild(type: org.gradle.api.tasks.Exec, description: "compile JNI by NDK") {
commandLine "/Users/wujian/Library/Android/sdk/ndk-bundle/ndk-build",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
tasks.withType(JavaCompile) {
compileTask->compileTask.dependsOn ndkBuild
}
这种方法并不推荐,实际上个人觉得还是AS支持的ndk和cmake最佳,右击app
,选择构建工具,link一下CMakeLists.txt
或者Android.mk
就行了。以后找到问题所在还是会切换过去的。
无非就是一个链接问题,前后折腾了五天,现在总结的话,大致也只用这么多可以被写下来的东西。不过,想到今后从模型到demo的可以省下的大把时间,还是一件值得的事情。