最近在做语音相关的项目,需要用到WebRTC的APM(Audio Processing Module)模块,APM的主要功能包括音频的3A(AEC、AGC、NS)算法处理以及VAD等。原本打算以源码的方式依赖进来,但是发现后续如果需要升级不好维护,而且需要编写较多的mk文件,所以就考虑是否可以以静态库的方式引进来。试验过后,发现此方案可行,而且并不困难(当然探索过程是比较坎坷的)。
WebRTC Android版本只能在Linux环境下编译,按照官方的指引下载好源码,发现其使用GN + Ninja来构建编译系统,查看webrtc.gni文件,可以看到里面使用rtc_static_library这个模板来描述一个静态库,例如在webrtc_android/src/modules/audio_processing/Build.GN中,就使用了这个模板来编译APM静态库:
rtc_static_library("audio_processing") {
visibility = [ "*" ]
configs += [ ":apm_debug_dump" ]
sources = [
"audio_processing_impl.cc",rtc_static_library("audio_processing")
...
]
...
}
按照指引,在根目录分别执行如下两条命令即可编译整个WebRTC库:
gn gen out/Debug --args='target_os="android" target_cpu="arm"'
ninja -C out/Debug
第一条命令是用gn工具根据Build.GN描述文件来生成编译文件build.ninja,build.ninja类似于Android.mk;第二条命令是用ninja来编译源文件。ninja编译时可以带上目标名字,所以我们试下编译APM模块:
ninja -C out/Debug audio_processing
发现确实可以生成APM静态库,输出文件为out/Debug/obj/modules/audio_processing/libaudio_processing.a。到此感觉像是要成功了,迫不及待地把这个静态库导入到自己的项目中使用。Android使用第三方静态库,需要编写一个mk文件,将第三方静态库转成一个PREBUILT_STATIC_LIBRARY类型的库,然后才能在其它mk文件中通过LOCAL_STATIC_LIBRARIES来依赖。此外,还需把相关的头文件拷过来。转换mk文件下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_ARM_MODE := arm
LOCAL_MODULE := audio_processing
LOCAL_SRC_FILES := libaudio_processing.a
include $(PREBUILT_STATIC_LIBRARY)
然后在需要依赖APM的模块中添加:
LOCAL_STATIC_LIBRARIES += libaudio_processing
APM相关的头文件比较多,而且分散在不同的模块,所以这里就偷懒把所有WebRTC的头文件都拷贝到项目里了。可用以下命令进行拷贝,并且保留目录结构,后面的./out/include路径需要提前创建,或者换成你所期望的路径也行:
find . -name "*.h" -type f -exec cp --parents {} ./out/include \;
准备完成后用ndk-build进行编译,竟然报错了。
error: undefined reference to 'WebRtcVad_Init'
error:undefined reference to 'WebRtcVad_Create'
error: undefined reference to 'WebRtcVad_Free'
...
error: undefined reference to 'WebRtcAecm_Init'
error: undefined reference to 'WebRtcAecm_Create'
...
error: undefined reference to 'WebRtcNsx_Init'
...
error: undefined reference to 'webrtc::Resampler::Resampler()'
提示APM里面的函数或者类没定义,只能看下编译出来的libaudio_processing.a文件中是否有对应的定义。
nm -u libaudio_processing.a
摘取部分内容:
out/Debug/obj/modules/audio_processing/audio_processing/voice_detection_impl.o:
U WebRtcVad_Create
U WebRtcVad_Free
U WebRtcVad_Init
U WebRtcVad_Process
U WebRtcVad_set_mode
U _ZN3rtc18webrtc_checks_impl8FatalLogEPKciS2_PKNS0_12CheckArgTypeEz
...
out/Debug/obj/modules/audio_processing/audio_processing/echo_control_mobile_impl.o:
U WebRtcAecm_BufferFarend
U WebRtcAecm_Create
U WebRtcAecm_Free
U WebRtcAecm_Init
U WebRtcAecm_Process
U WebRtcAecm_set_config
...
out/Debug/obj/modules/audio_processing/audio_processing/noise_suppression_impl.o:
U WebRtcNsx_Create
U WebRtcNsx_Free
U WebRtcNsx_Init
U WebRtcNsx_Process
U WebRtcNsx_noise_estimate
U WebRtcNsx_num_freq
U WebRtcNsx_set_policy
...
发现里面已经有相关的函数定义了,只是在生成.a文件时,把.o文件的路径也弄进来了,所以感觉把路径去掉应该可行。而且,笔者发现这个.a文件的大小只有76k,所以应该是没有把依赖的其它库给编进来。怎么解决?思来想去,决定自己用.o文件生成.a文件。把out/Debug/obj下所有的.o文件拷贝到一个临时目录,然后用ar命令生成静态库。首先是拷贝:
find out/Debug/obj -name *.o | xargs -n1 -I {} cp {} ./tmp
然后进到tmp目录执行如下命令生成静态库:
ar rc libaudio_processing.a *.o
现在这个静态库有7.8M,感觉有点靠谱了。把这个手动生成的静态库放到工程里再次编译,发现还是报错:
../../rtc_base/checks.cc:108: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::append(char const*)'
../../rtc_base/checks.cc:94: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::append(char const*)'
../../rtc_base/checks.cc:101: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::append(char const*, unsigned int)'
../../buildtools/third_party/libc++/trunk/include/string:2547: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::append(char const*, unsigned int)'
../../rtc_base/checks.cc:141: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()'
../../rtc_base/checks.cc:141: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()'
../../rtc_base/checks.cc:142: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::append(char const*)'
../../buildtools/third_party/libc++/trunk/include/string:961: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::resize(unsigned int, char)'
../../rtc_base/logging.cc:77: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > std::__1::operator+<char, std::__1::char_traits<char>, std::__1::allocator<char> >(char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)'
../../rtc_base/logging.cc:77: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()'
../../rtc_base/logging.cc:77: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()'
...
这些错误看起来像是string这个类的一些错误,string是c++库里的类,所以就考虑是否编译APM的ndk版本和App中的ndk版本对不上导致的,尝试使用相同版本的ndk去编译发现还是有问题。后来查资料发现WebRTC里用的c++库是libc++,而目前Android中使用的c++库是libstdc++,所以是c++库不一样导致的。解决办法就是同步c++库的版本:App使用libc++库 或者 WebRTC使用libstdc++库,笔者选择的是后者。参考这篇文章,只需在执行gn命令时,加入use_custom_libcxx=false这个参数就可以了。所以第一条编译的命令就变成:
gn gen out/Debug --args='target_os="android" target_cpu="arm" use_custom_libcxx=false'
按照手动生成静态库的步骤生成libaudio_processing.a,然后再次在App中编译,发现没问题了,跑起来功能也正常,所以到此算是完成了APM模块的单独编译。举一反三,其它WebRTC的静态库也可以通过这种方式来编译,所以笔者写了一个脚本放到GitHub上,只需把这个脚本放到WebRTC源码的根目录,执行它时带上模块名即可编译对应的静态库,生成的静态库和头文件会在out目录下。