SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
运行脚本就会生产如libavcodec.so
的文件了,这样做是可以让我们单独使用每个库,但是通常我们不需要这么做。
通常我们不想要那么多so文件,只需要一个libffmpeg.so
文件,那么可以在make install
命令只有加上一个命令来链接各个.a文件成为一个so文件,也就是说要编译为:
$TOOLCHAIN /bin/arm-linux-androideabi-ld \
-rpath-link = $PLATFORM /usr/lib \
-L $PLATFORM /usr/lib \
-L $PREFIX /lib \
-soname libffmpeg.so -shared -nostdlib -Bsymbolic --whole-archive --no-undefined -o \
$PREFIX /libffmpeg.so \
libavcodec/libavcodec.a \
libavdevice/libavdevice.a \
libavfilter/libavfilter.a \
libavformat/libavformat.a \
libavutil/libavutil.a \
libpostproc/libpostproc.a \
libswresample/libswresample.a \
libswscale/libswscale.a \
-lc -lm -lz -ldl -llog --dynamic-linker = /system/bin/linker \
$TOOLCHAIN /lib/gcc/arm-linux-androideabi/4.9/libgcc.a
我这里配置的cpu是armeabi-v7a
,一般android平台是最低支持v5的,但是我发现大部分机型都是v7的了,就算低端机也是v6以上的了,如果要修改为v6,只需要改这些配置即可:
CPU = armv6
OPTIMIZE_CFLAGS = "-DCMP_HAVE_VFP -mfloat-abi=softfp -mfpu=vfp -marm -march= $CPU -Wno-multichar -fno-exceptions"
--disable-neon
--disable-thumb
最后把so和头文件复制到jni目录,就可以在android中使用了。
3. What’s libmp3lame
当我们想要编码为mp3的时候,就需要用到mp3的编码库了,ffmpeg是一个大框架,里面虽然有很多编解码的库,但是也有很多是没有的,但是因为它是一个框架,所以很容易往里面集成东西。由于业务的需要,我做一个转码为mp3的功能,本来以为ffmpeg直接就支持,没想到是没有的,需要集成这个libmp3lame 库。官网上没有教程编译android平台的lame,单纯用configure和make命令编译的应该不能使用,需要像ffmpeg那样配置为ndk的交叉编译才可以。好在lame的代码不多,可以直接配置为jni项目直接用ndk编译。
3.1 编译libmp3lame
创建一个lame目录,里面创建一个jni目录,然后去官网上下载最新版3.99,解压以后把里面的libmp3lame和include目录复制到jni目录下,然后在jni目录下创建Application.mk和Android.mk。
Application.mk
APP_BUILD_SCRIPT := $( call my-dir) /Android.mk
APP_PROJECT_PATH := $( call my-dir)
APP_MODULES:= mp3lame
APP_PLATFORM:= android-14
APP_ABI:= armeabi armeabi-v7a x86 mips
Android.mk
LOCAL_PATH := $( call my-dir)
include $( CLEAR_VARS)
LOCAL_MODULE := mp3lame
LOCAL_CFLAGS := -DSTDC_HEADERS
LOCAL_SRC_FILES := ./libmp3lame/bitstream.c ./libmp3lame/encoder.c ./libmp3lame/fft.c ./libmp3lame/gain_analysis.c \
./libmp3lame/id3tag.c ./libmp3lame/lame.c ./libmp3lame/mpglib_interface.c ./libmp3lame/newmdct.c \
./libmp3lame/presets.c ./libmp3lame/psymodel.c ./libmp3lame/quantize.c ./libmp3lame/quantize_pvt.c \
./libmp3lame/reservoir.c ./libmp3lame/set_get.c ./libmp3lame/tables.c ./libmp3lame/takehiro.c \
./libmp3lame/util.c ./libmp3lame/vbrquantize.c ./libmp3lame/VbrTag.c ./libmp3lame/version.c
LOCAL_C_INCLUDES := $( LOCAL_PATH) /libmp3lame $( LOCAL_PATH) /include
ifeq ( $( TARGET_ARCH_ABI) , armeabi-v7a)
# 采用NEON优化技术
LOCAL_ARM_NEON := true
LOCAL_CFLAGS += -mfpu = neon -mfpu = vfpv3-d16
endif
ifeq ( $( TARGET_ARCH_ABI) , armeabi)
LOCAL_CFLAGS += -marm -mfpu = vfp -mfpu = vfpv3 -DCMP_HAVE_VFP
endif
include $( BUILD_STATIC_LIBRARY)
然后命令行中切换到jni目录运行ndk-build
,当然我们是完全不用创建jni目录的,直接解压lame-3.99以后在里面创建Application.mk和Android.mk,不过直接运行ndk-build是会报错的:
Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/Users/zhgeaits/develop/android-ndk-r11c/build/core/build-local.mk:151: *** Android NDK: Aborting . Stop.
是因为ndk-build脚本检查当前目录不是jni目录就判断不是android项目了,然后报错,我们可以这样运行即可:
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk
此时就算成功运行ndk-build,依然会报另外一个错:
In file included from /Users/zhgeaits/develop/resources/ffmpeg/lame/jni/./libmp3lame/bitstream.c:36:0:
/Users/zhgeaits/develop/resources/ffmpeg/lame/jni/./libmp3lame/util.h:574:12: error: unknown type name 'ieee754_float32_t'
extern ieee754_float32_t fast_log2(ieee754_float32_t x);
/Users/zhgeaits/develop/resources/ffmpeg/lame/jni/./libmp3lame/util.h:574:40: error: unknown type name 'ieee754_float32_t'
extern ieee754_float32_t fast_log2(ieee754_float32_t x);
那是因为android里面不知道ieee754_float32_t
是什么,并没有这样的宏定义,我们只要在util.h
里面修改为float
即可。再次运行ndk-build,就成功了,在obj目录下生产了libmp3lame.a
的静态库了。不管是静态库还是动态库都是已经可以在jni 里面使用的了。但是要集成到ffmpeg中去,我们还是使用静态库好。
3.2 Build ffmpeg with libmp3lame
在ffmpeg的目录下运行./configure --help | grep libmp3lame
,可以发现有--enable-libmp3lame
的选项,说明ffmpeg是已经支持lame的了,完全可以集成进去。于是我们修改编译脚本:
--enable-libmp3lame
--enable-encoder = libmp3lame
ARMEABI = armeabi-v7a
LAMEDIR = /Users/zhgeaits/develop/resources/ffmpeg/lame
EXTRA_LDFLAGS = "-L $LAMEDIR /obj/local/ $ARMEABI "
EXTRA_CFLAGS = "-O2 -fpic -I $PLATFORM /usr/include -I $LAMEDIR /jni/libmp3lame -I $LAMEDIR /jni/include $OPTIMIZE_CFLAGS
"
注意上面$LAMEDIR/obj/local/$ARMEABI指向的是libmp3lame.a位置。
重新运行脚本会发现报错ERROR: libmp3lame >= 3.98.3 not found
,我们去查看config.log
会发现:
fatal error: lame/lame.h: No such file or directory
于是在include目录建立lame目录,然后把lame.h复制进去即可。最后就可以编译成功了,注意如果是编译为一个so库,别忘了把libmp3lame.a也要链接进去。
3.3 JNI中调用lame接口
不管是直接用libmp3lame的库,还是编译进去ffmpeg里面,只要用到jni里面去,我们都可以直接调lame的接口了。首先添加下面的头文件:
lame_global_flags * initializeDefault ( JNIEnv * env );
lame_global_flags * initialize ( JNIEnv * env , jint inSamplerate , jint outChannel ,
jint outSamplerate , jint outBitrate , jfloat scaleInput , jint mode , jint vbrMode ,
jint quality , jint vbrQuality , jint abrMeanBitrate , jint lowpassFreq , jint highpassFreq ,
jstring id3tagTitle , jstring id3tagArtist , jstring id3tagAlbum ,
jstring id3tagYear , jstring id3tagComment );
jint encode ( JNIEnv * env , lame_global_flags * glf , jshortArray buffer_l , jshortArray buffer_r ,
jint samples , jbyteArray mp3buf );
jint encodeBufferInterleaved ( JNIEnv * env , lame_global_flags * glf ,
jshortArray pcm , jint samples , jbyteArray mp3buf );
jint flush ( JNIEnv * env , lame_global_flags * glf , jbyteArray mp3buf );
void close_vidmate_lame ( lame_global_flags * glf );
然后实现这些接口:
lame_global_flags * glf ;
lame_global_flags * initializeDefault ( JNIEnv * env ) {
lame_global_flags * glf = lame_init ();
lame_init_params ( glf );
return glf ;
lame_global_flags * initialize ( JNIEnv * env , jint inSamplerate , jint outChannel ,
jint outSamplerate , jint outBitrate , jfloat scaleInput , jint mode , jint vbrMode ,
jint quality , jint vbrQuality , jint abrMeanBitrate , jint lowpassFreq , jint highpassFreq ,
jstring id3tagTitle , jstring id3tagArtist , jstring id3tagAlbum ,
jstring id3tagYear , jstring id3tagComment ) {
lame_global_flags * glf = lame_init ();
lame_set_in_samplerate ( glf , inSamplerate );
lame_set_num_channels ( glf , outChannel );
lame_set_out_samplerate ( glf , outSamplerate );
lame_set_brate ( glf , outBitrate );
lame_set_quality ( glf , quality );
lame_set_scale ( glf , scaleInput );
lame_set_VBR_q ( glf , vbrQuality );
lame_set_VBR_mean_bitrate_kbps ( glf , abrMeanBitrate );
lame_set_lowpassfreq ( glf , lowpassFreq );
lame_set_highpassfreq ( glf , highpassFreq );
switch ( mode ) {
case 0 :
lame_set_mode ( glf , STEREO );
break ;
case 1 :
lame_set_mode ( glf , JOINT_STEREO );
break ;
case 3 :
lame_set_mode ( glf , MONO );
break ;
case 4 :
lame_set_mode ( glf , NOT_SET );
break ;
default:
lame_set_mode ( glf , NOT_SET );
break ;
switch ( vbrMode ) {
case 0 :
lame_set_VBR ( glf , vbr_off );
break ;
case 2 :
lame_set_VBR ( glf , vbr_rh );
break ;
case 3 :
lame_set_VBR ( glf , vbr_abr );
break ;
case 4 :
lame_set_VBR ( glf , vbr_mtrh );
break ;
case 6 :
lame_set_VBR ( glf , vbr_default );
break ;
default:
lame_set_VBR ( glf , vbr_off );
break ;
const jchar * title = NULL ;
const jchar * artist = NULL ;
const jchar * album = NULL ;
const jchar * year = NULL ;
const jchar * comment = NULL ;
if ( id3tagTitle ) {
title = ( * env ) -> GetStringChars ( env , id3tagTitle , NULL );
if ( id3tagArtist ) {
artist = ( * env ) -> GetStringChars ( env , id3tagArtist , NULL );
if ( id3tagAlbum ) {
album = ( * env ) -> GetStringChars ( env , id3tagAlbum , NULL );
if ( id3tagYear ) {
year = ( * env ) -> GetStringChars ( env , id3tagYear , NULL );
if ( id3tagComment ) {
comment = ( * env ) -> GetStringChars ( env , id3tagComment , NULL );
if ( title || artist || album || year || comment ) {
id3tag_init ( glf );
if ( title ) {
id3tag_set_title ( glf , ( const char * ) title );
( * env ) -> ReleaseStringChars ( env , id3tagTitle , title );
if ( artist ) {
id3tag_set_artist ( glf , ( const char * ) artist );
( * env ) -> ReleaseStringChars ( env , id3tagArtist , artist );
if ( album
) {
id3tag_set_album ( glf , ( const char * ) album );
( * env ) -> ReleaseStringChars ( env , id3tagAlbum , album );
if ( year ) {
id3tag_set_year ( glf , ( const char * ) year );
( * env ) -> ReleaseStringChars ( env , id3tagYear , year );
if ( comment ) {
id3tag_set_comment ( glf , ( const char * ) comment );
( * env ) -> ReleaseStringChars ( env , id3tagComment , comment );
lame_init_params ( glf );
return glf ;
jint encode ( JNIEnv * env , lame_global_flags * glf , jshortArray buffer_l , jshortArray buffer_r ,
jint samples , jbyteArray mp3buf ) {
jshort * j_buffer_l = ( * env ) -> GetShortArrayElements ( env , buffer_l , NULL );
jshort * j_buffer_r = ( * env ) -> GetShortArrayElements ( env , buffer_r , NULL );
const jsize mp3buf_size = ( * env ) -> GetArrayLength ( env , mp3buf );
jbyte * j_mp3buf = ( * env ) -> GetByteArrayElements ( env , mp3buf , NULL );
int result = lame_encode_buffer ( glf , j_buffer_l , j_buffer_r , samples , j_mp3buf , mp3buf_size );
( * env ) -> ReleaseShortArrayElements ( env , buffer_l , j_buffer_l , 0 );
( * env ) -> ReleaseShortArrayElements ( env , buffer_r , j_buffer_r , 0 );
( * env ) -> ReleaseByteArrayElements ( env , mp3buf , j_mp3buf , 0 );
return result ;
jint encodeBufferInterleaved ( JNIEnv * env , lame_global_flags * glf ,
jshortArray pcm , jint samples , jbyteArray mp3buf ) {
jshort * j_pcm = ( * env ) -> GetShortArrayElements ( env , pcm , NULL );
const jsize mp3buf_size = ( * env ) -> GetArrayLength ( env , mp3buf );
jbyte * j_mp3buf = ( * env ) -> GetByteArrayElements ( env , mp3buf , NULL );
int result = lame_encode_buffer_interleaved ( glf , j_pcm , samples , j_mp3buf , mp3buf_size );
( * env ) -> ReleaseShortArrayElements ( env , pcm , j_pcm , 0 );
( * env ) -> ReleaseByteArrayElements ( env , mp3buf , j_mp3buf , 0 );
return result ;
jint flush ( JNIEnv * env , lame_global_flags * glf , jbyteArray mp3buf ) {
const jsize mp3buf_size = ( * env ) -> GetArrayLength ( env , mp3buf );
jbyte * j_mp3buf = ( * env ) -> GetByteArrayElements ( env , mp3buf , NULL );
int result = lame_encode_flush ( glf , j_mp3buf , mp3buf_size );
( * env ) -> ReleaseByteArrayElements ( env , mp3buf , j_mp3buf , 0 );
return result ;
void close_vidmate_lame ( lame_global_flags * glf ) {
lame_close ( glf );
glf = NULL ;
然后剩下的是在java层定义JNI的接口来调用即可,首先是初始化init
,然后encode
,最后flush
和close
,流程都非常简单,注意到的是encodeBufferInterleaved()
函数是交叉存储的意思,即不分左右音轨的意思,有时候我们解码拿到的数据就是交叉存储的,不用专门去拆分左右音轨了。另外,编码是不支持多线程的,可能需要自己去修改源码,应该就是全局变量的问题。
4. What’s libshine
原本使用libmp3lame这个库就已经足够的了,它已经是业界的权威mp3编码库,非常的标准和稳定,但是效率方面太慢了,就算我针对arm架构cpu的各种特性,neon,vfp等等编译加速,在高端手机上还是蛮快的,但是一旦遇到低端手机以后就非常非常的慢!不能满足需求,于是各种网上google和Github,发现了shine ,根据说明和测试,它是超级快的一个mp3编码库,特别是在没有FPU的cpu上,使用的是Fixed-point算法,具体我也不懂,不过使用以后,性能真的快了很多!
4.1 编译libshine
可以看到Github上的源码它直接是支持android的了,clone下来代码,发现里面有一个Makefile.am文件,去掉后缀名,Makefile文件里面已经有android的目标了,我们修改Binary.mk和Library.mk文件,去掉formatbits.c,因为src目录下并没有这个文件的了。然后直接在shine目录下执行make命令,即可完成编译了,可以看到在android目录生成了libshine.so了。当然我们可以修改Library.mk文件,生成静态库libshine.a而不是so库,不管怎么样,都是可以直接在jni使用的了。
另外,我们完全可以像libmp3lame那样,把代码复制出来,建立jni目录,创建Android.mk和Application.mk文件,然后自行编译。
4.2 Build ffmpeg with libshine
比较麻烦的就是要把libshine集成到ffmpeg里面去了。我们在ffmpeg目录运行./configure --help | grep shine
,发现ffmpeg已经和libmp3lame那样支持的了,我们需要模仿libmp3lame那样修改编译脚本:
--enable-libshine
--enable-encoder = libshine
ARMEABI = armeabi-v7a
SHINEDIR = /Users/zhgeaits/develop/resources/ffmpeg/shine
EXTRA_LDFLAGS = "-L $SHINEDIR /obj/local/ $ARMEABI "
EXTRA_CFLAGS = "-O2 -fpic -I $PLATFORM /usr/include -I $SHINEDIR /jni/shine $OPTIMIZE_CFLAGS "
注意到上面的脚本是没有包含libmp3lame的,当然我们是完全可以同时包含两个库的。另外--enable-encoder=libshine
这个一定要有,不然ffmpeg是不会编译进去shine库的。然后开始编译,你会发现报错找不到shine,然后我们去看config.log
:
check_pkg_config shine shine/layer3.h shine_encode_buffer
false --exists --print-errors shine
ERROR: shine not found using pkg-config
难道又是头文件位置不对?我去修改layer3.h的位置,依然报错!我们去查看configure文件,所有地方lame和shine都是一样的配置,唯独这里不同:
enabled libmp3lame && require “libmp3lame >= 3.98.3” lame/lame.h lame_set_VBR_quality -lmp3lame
而shine是这样的:
enabled libshine && require_pkg_config shine shine/layer3.h shine_encode_buffer
我再去看libavcodec/Makefile
和libavcodec/allcodecs.c
,里面两者的配置都是一样的,我依旧不明白为什么上面的配置不同,我目前也不懂pkg-config是个上面东东!这留给以后去学习好了,我就尝试修改为这样:
enabled libshine && require “shine” shine/layer3.h shine_encode_buffer -lshine
再次运行脚本,居然成功了!看来并没有多大的区别和关系啊。我猜测原本的配置是用于检查编译有没有pkg-config的,而这个应该是pc平台,在android平台并找不到。
另外,ffmpeg是怎么把libmp3lame和libshine集成过去的?我们看到libavcodec/libmp3lame.c
和libavcodec/libshine.c
文件,里面调用了编码的接口,实现了ffmpeg的标准接口,瞬间明白,这就像插件一样把库插进去了!如果我们没有--enable-encoder=libshine
这样的配置,那么就不会编译libshine.c文件了!
4.3 JNI中调用shine接口
不管是直接用libshine的库,还是编译进去ffmpeg里面,只要用到jni里面去,我们都可以直接调shine的接口了。关于shine的api使用,并没有文档,不过由于比较简单,代码也不多,加上如果对音频多媒体知识有点了解,基本是没有困难的。我们可以查看源码里面main.c
文件简单的使用把wav转换mp3,还可以查看layer3.h
头文件对api的说明。
shine_config_t config;
shine_t s;
int written;
void zhangge_init_shine( int channels, int samplerate) {
shine_set_config_mpeg_defaults( &config.mpeg) ;
config.wave.channels = channels;
config.wave.samplerate = samplerate;
/* Set to stereo mode if wave data is stereo, mono otherwise. * /
if ( config.wave.channels > 1)
config.mpeg.mode = STEREO;
config.mpeg.mode = MONO;
s = shine_initialise( &config) ;
int get_samples_per_pass() {
return shine_samples_per_pass( s) ;
unsigned char * zhangge_shine_encode_buffer( int16_t ** data) {
return shine_encode_buffer( s, data, &written) ;
unsigned char * zhangge_shine_encode_buffer_interleaved( int16_t * data, int * byteEncode) {
unsigned char * result = shine_encode_buffer_interleaved( s, data, byteEncode) ;
return result;
unsigned char * zhangge_shine_flush() {
return shine_flush( s, &written) ;
void zhangge_shine_close() {
return shine_close( s) ;
JNIEXPORT void JNICALL Java_org_zhangge_ShineCodec_shineInit
( JNIEnv * env , jclass cls, jint channels, jint samplerate) {
zhangge_init_shine( channels, samplerate) ;
JNIEXPORT jint JNICALL Java_org_zhangge_ShineCodec_shineSamplesPerPass
( JNIEnv * env , jclass cls) {
int sample_per_pass = get_samples_per_pass() ;
return sample_per_pass;
JNIEXPORT void JNICALL Java_org_zhangge_ShineCodec_shineFlush
( JNIEnv * env , jclass cls) {
zhangge_shine_flush() ;
JNIEXPORT void JNICALL Java_org_zhangge_ShineCodec_shineClose
( JNIEnv * env , jclass cls) {
zhangge_shine_close() ;
JNIEXPORT jbyteArray JNICALL Java_org_zhangge_ShineCodec_shineEncodeInterleaved
( JNIEnv * env , jclass cls, jshortArray data) {
short * pcm = ( * env ) ->GetShortArrayElements( env , data, NULL) ;
unsigned char * mp3buf = zhangge_shine_encode_buffer_interleaved( pcm, &written) ;
jbyteArray result = ( * env ) ->NewByteArray( env , written) ;
( * env ) ->SetByteArrayRegion( env , result, 0, written, mp3buf) ;
( * env ) ->ReleaseShortArrayElements( env , data, pcm, 0) ;
return result;
上面就是核心的代码了,对于java层的使用就更简单了,但是它不并lame那么简单,可以任意传一个buffer下去编码,这个buffer的大小需要通过调用get_samples_per_pass()来获取,而written参数是编码成功以后的大小。