转载请注明出处:http://blog.csdn.net/allen315410/article/details/42456661

在本专栏的前面几篇博客中讲述了一些Android NDK开发的基础,从环境搭建一直到利用JNI进行Java端和C端代码的互相调用,并且的讲解的Demo也是很简单易懂的,相信掌握前面博客的大部分内容,就可以着手在实际项目中利用JNI进行NDK开发了,那么既然基础过了,接下来我在这里尝试去使用真实项目中去。我们知道,C语言因为高效,而且又是最早期的高级编程之一,一直存活至今近40年了,所以很多用C开发出来高效类库是可以被复用的,这样不仅做到高效率,而且减少了项目开发周期。在这里我找到了一个关于音频文件转码的最常用的类库——LAME,这篇博客就是基于LAME开发一个wav音频文件转码成mp3音频文件的小项目。


一、LAME简介

LAME是目前最好的MP3编码引擎。LAME编码出来的MP3音色纯厚、空间宽广、低音清晰、细节表现良好,它独创的心理音响模型技术保证了CD音频还原的真实性,配合VBR和ABR参数,音质几乎可以媲美CD音频,但文件体积却非常小。对于一个免费引擎,LAME的优势不言而喻。关于LAME的介绍可以在百度百科,维基百科中找到,我在这里不再赘述了,但是要知道LAME可以帮助我们将wav无损音频文件转码成mp3这种体积相对较小的音频格式文件。

注:关于wav和mp3是一系列的音频编解码算法,具体我也不是特别清楚它的厉害之处和实现原理,想要深入了解音频编码还需要找一些相关的资料文档来读,但是不清楚这些也不妨碍我们开发这样的一个项目。

LAME的源码是托管到sourceforge.net上的,我们开发一个基于LAME的项目,就不得不下载其源码用于编译。

LAME主页:http://lame.sourceforge.net/

LAME下载:http://sourceforge.net/projects/lame/files/lame/3.99/

目前LAME的最新版本是3.99.5,不能连上SourceForge主页的朋友可以点击csdn下载,我已经上传,地址是:http://download.csdn.net/detail/lee_tianya/8332357


二、编写本地方法

开发这个转码项目的时候,做一下简单的处理,编写两个简单的Native方法,一个是convertmp3(String wav,String mp3),这个方法是主要的Native方法,目的是将从Java层获取到的两个音频文件的路径已字符串形式传递给C端,C端拿到这两个路径,就可以进行读写和编解码操作了,另外一个Native方法是getLameVersion(),该方法是获取LAME的版本号的,返回值为字符串,用处是校验在本地是否成功编译.so文件。源码如下

/** * wav转换成mp3的本地方法 *  * @param wav * @param mp3 */public native void convertmp3(String wav, String mp3);/** * 获取LAME的版本信息 *  * @return */public native String getLameVersion();

三、编译头文件

通过前面几篇博客学习,我们知道在Java层中定义完我们需要的Native方法后,需要使用JDK提供的javah命令编译C文件中需要的方法签名,具体做法是1,如果你使用的是jdk1.6及以下版本,将目录切换到工程目录下的classes目录;2,如果你使用的jdk1.7版本,请将目录切换到工程目录下的src目录,执行如下的javah命令:、


好!看到没有报错的话,就说明我们的方法签名文件以及生成了,在src目录下找到这个方法签名的文件,在工程目录下新建一个jni的目录,将方法签名文件拷贝进jni目录之中,我们先打开这个文件看看内容好了。

/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_lame_MainActivity */#ifndef _Included_com_example_lame_MainActivity#define _Included_com_example_lame_MainActivity#ifdef __cplusplusextern "C" {#endif/* * Class:     com_example_lame_MainActivity * Method:    convertmp3 * Signature: (Ljava/lang/String;Ljava/lang/String;)V */JNIEXPORT void JNICALL Java_com_example_lame_MainActivity_convertmp3  (JNIEnv *, jobject, jstring, jstring);/* * Class:     com_example_lame_MainActivity * Method:    getLameVersion * Signature: ()Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_com_example_lame_MainActivity_getLameVersion  (JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif

四、将LAME的源码导入jni目录

项目进行到这里就要显得特别重要也特别小心了,因为此时需要引用开源库LAME到本项目中并且还要重新编译成适用于本平台的库。

1,将下载来的LAME源码解压到本地,打开解压后的目录,找到libmp3lame目录,将该目录下的所有的文件都拷贝到jni目录下。

2,剔除不必要的文件目录。例如i386这个目录要删除,还要删除几个非.h,.c作为扩展名的文件,已经Linux下的批处理文件,因为这些文件都是Android平台下非必要的。

3,引入lame.h头文件。在LAME解压目录下找到include目录,将其下的lame.h头文件拷贝到jni目录下,如果这个目录没有被引入,会报如下的错误


4,修改util.h的源码。在JNI目录下找到util.h文件,在574行找到ieee754_float32_t数据类型,将其修改为float类型,因为ieee754_float32_t是Linux或者是Unix下支持的数据 类型,在Android下并不支持。如果不修改,则编译源码的时候会报如下错误



五、编写本地C语言代码,实现音频文件的转码

关于怎么编写这段C语言代码,确实是有些难度,需要一定的C语言知识,而且还要保证能够看懂lame.h文件中提供的API和注释,在这里我就不在说明了,因为这个事情说起来那就复杂多了,单单就一篇博客是将不完整的,下面贴出C语言代码,代码中提供了较为详尽的注释,带着注释去看代码,便于理解的!

#include<stdio.h>#include<jni.h>#include<malloc.h>#include<string.h>#include<lame.h>#include"com_example_lame_MainActivity.h"#include<android/log.h>#define LOG_TAG "System.out.c"#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)/** * 返回值 char* 这个代表char数组的首地址 *  Jstring2CStr 把java中的jstring的类型转化成一个c语言中的char 字符串 */char* Jstring2CStr(JNIEnv* env, jstring jstr) {char* rtn = NULL;jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //Stringjstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一个java字符串 "GB2312"jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes","(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,strencode); // String .getByte("GB2312");jsize alen = (*env)->GetArrayLength(env, barr); // byte数组的长度jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);if (alen > 0) {rtn = (char*) malloc(alen + 1); //"\0"memcpy(rtn, ba, alen);rtn[alen] = 0;}(*env)->ReleaseByteArrayElements(env, barr, ba, 0); //return rtn;}int flag = 0;/** * wav转换mp3 */JNIEXPORT void JNICALL Java_com_example_lame_MainActivity_convertmp3(JNIEnv * env, jobject obj, jstring jwav, jstring jmp3) {char* cwav =Jstring2CStr(env,jwav) ;char* cmp3=Jstring2CStr(env,jmp3);LOGI("wav = %s", cwav);LOGI("mp3 = %s", cmp3);//1.打开 wav,MP3文件FILE* fwav = fopen(cwav,"rb");FILE* fmp3 = fopen(cmp3,"wb");short int wav_buffer[8192*2];unsigned char mp3_buffer[8192];//1.初始化lame的编码器lame_t lame =  lame_init();//2. 设置lame mp3编码的采样率lame_set_in_samplerate(lame , 44100);lame_set_num_channels(lame,2);// 3. 设置MP3的编码方式lame_set_VBR(lame, vbr_default);lame_init_params(lame);LOGI("lame init finish");int read ; int write; //代表读了多少个次 和写了多少次int total=0; // 当前读的wav文件的byte数目do{if(flag==404){return;}read = fread(wav_buffer,sizeof(short int)*2, 8192,fwav);total +=  read* sizeof(short int)*2;LOGI("converting ....%d", total);publishJavaProgress(env,obj,total);// 调用java代码 完成进度条的更新if(read!=0){write = lame_encode_buffer_interleaved(lame,wav_buffer,read,mp3_buffer,8192);//把转化后的mp3数据写到文件里fwrite(mp3_buffer,sizeof(unsigned char),write,fmp3);}if(read==0){lame_encode_flush(lame,mp3_buffer,8192);}}while(read!=0);LOGI("convert  finish");lame_close(lame);fclose(fwav);fclose(fmp3);}/** * 调用java代码 更新程序的进度条 */void publishJavaProgress(JNIEnv * env, jobject obj, jint progress) {// 1.找到java的MainActivity的classjclass clazz = (*env)->FindClass(env, "com/example/lame/MainActivity");if (clazz == 0) {LOGI("can't find clazz");}LOGI(" find clazz");//2 找到class 里面的方法定义jmethodID methodid = (*env)->GetMethodID(env, clazz, "setConvertProgress","(I)V");if (methodid == 0) {LOGI("can't find methodid");}LOGI(" find methodid");//3 .调用方法(*env)->CallVoidMethod(env, obj, methodid, progress);}/** * 获取LAME的版本号 */JNIEXPORT jstring JNICALL Java_com_example_lame_MainActivity_getLameVersion(JNIEnv * env, jobject obj) {return (*env)->NewStringUTF(env, get_lame_version());}
上面新建了一个convert.c的C源文件,源码中首先引用一些相关的类库来使用,例如<stdio.h><jni.h><malloc.h><string.h>这几个是C的标准类库,代码中有需要就引入。接下来,我们还需要引用上述第三主题中编译好的方法签名文件com_example_lame_MainActivity.h,这个文件定义了Native方法的签名,方便编译器能够找到相对应的Native方法,还有一个非常重要的头文件lame.h,这个项目中用的就是LAME,所以这个lame.h的头文件是必须引用上的,最后就是log.h的引入和定义了,都是固定的内容,看源码就好了。


六、编写配置文件和交叉编译

完成上述步骤以及源码的编写之后,剩下的就是配置文件了。首先看看Android.mk文件,这个就麻烦了,因为上面我们将LAME整个源码文件全部拷贝到jni目录下了,这些文件都是要重新编译的,所以我们需要在Android.mk文件的LOCAL_SRC_FILES这个字段上,将所以的源码文件都要配置上去,不仅包括我们自定义的c文件,更有LAME中的c源码文件,由于LAME包含的文件太多了,所以在编写个LOCAL_SRC_FILES时要格外小心,写错一个就有可能编码不通过。下面是我的配置方案:

LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE    := convertLOCAL_SRC_FILES := convert.c bitstream.c fft.c id3tag.c mpglib_interface.c presets.c  quantize.c   reservoir.c tables.c  util.c  VbrTag.c encoder.c  gain_analysis.c lame.c  newmdct.c   psymodel.c quantize_pvt.c set_get.c  takehiro.c vbrquantize.c version.cLOCAL_LDLIBS += -lloginclude $(BUILD_SHARED_LIBRARY)
Application.mk

APP_PLATFORM := android-8
cygwin下交叉编译:

好!看到上图底部的提示,说明我们的.so是编译好了,由于文件较多,编译是需要花费一点时间的,而且上面报错了很多的warnning警告提示,但是不要紧,可以忽略,只要不是报错,那这个工程依然可以运行。


七、在Java层编写调用C端的代码

在编写Java写调用代码之前,我们考虑到因为编解码是个非常耗时的操作,所以这个操作是不能够放在主线程中执行的,必须得开启新的线程,又是因为这是一个耗时操作,所以为了良好的用户体验,我们需要在转码的过程中添加一个进度条对话框,提示一个转码的过程,关于这个进度条的进度问题也是交给C语言实现的,我们在Java中只需要定义一个这样的设置进度的方法,在C语言中回调这个java方法,将进度数据传递给Java中的进度条对话框即可。

public class MainActivity extends Activity {static {System.loadLibrary("convert");}private EditText et_wav;private EditText et_mp3;private ProgressDialog pd;/** * wav转换成mp3的本地方法 *  * @param wav * @param mp3 */public native void convertmp3(String wav, String mp3);/** * 获取LAME的版本信息 *  * @return */public native String getLameVersion();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);et_wav = (EditText) this.findViewById(R.id.et_wav);et_mp3 = (EditText) this.findViewById(R.id.et_mp3);pd = new ProgressDialog(this);}/** * wav转换mp3 */public void convert(View view) {final String mp3Path = et_mp3.getText().toString().trim();final String wavPath = et_wav.getText().toString().trim();File file = new File(wavPath);int size = (int) file.length();System.out.println("文件大小 " + size);if ("".equals(mp3Path) || "".equals(wavPath)) {Toast.makeText(MainActivity.this, "路径不能为空", 1).show();return;}pd.setMessage("转换中....");pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);pd.setMax(size); // 设置进度条的最大值pd.setCancelable(false);pd.show();// 转码是个耗时的操作,所以这里需要开启新线程去执行new Thread() {@Overridepublic void run() {convertmp3(wavPath, mp3Path);pd.dismiss();}}.start();}/** * 设置进度条的进度,提供给C语言调用 *  * @param progress */public void setConvertProgress(int progress) {pd.setProgress(progress);}/** * 获取LAME的版本号 */public void getVersion(View view) {Toast.makeText(MainActivity.this, getLameVersion(), 0).show();}}
注意需要添加权限两条:

<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

八、测试

好吧,写到这里总算是一个项目完成了,下面要进行一下测试了,先开一个arm的模拟器,然后运行一下工程。

1,测试版本号


2,测试wav转mp3

好,再来看看sdcard下的原wav音频文件和转码后的mp3文件

从上图可以看到我们的转码是可以使用的,原wav文件确实转码成了mp3文件,而且大小只有原文件的1/10了,导出这个mp3文件,用音频播放器播放一下,也是可以听得,文件并没有损坏。怎么样?项目做完了,你有没有兴趣试试呢?


源码请在这里下载



更多相关文章

  1. 一款常用的 Squid 日志分析工具
  2. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  3. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  4. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  5. storage/emulated/0/(Android中如何将文件选择器中返回的)....路
  6. SurfaceView应用浅析
  7. Android内、外存储 易混淆点剖析(/mnt/sdcard、/storage/sdcard0
  8. egret 发布微端项目
  9. android中Timer+TimerTask+Handler配合,重复定时执行某项任务(方法

随机推荐

  1. Android下拉刷新,上拉加载
  2. android上下左右滑动监听
  3. android创建文件夹
  4. Android文件读写,保存数据
  5. 在android 模拟器上安装 apt 文件
  6. Android修改文件权限
  7. Android出现java.net.SocketException: P
  8. Android(安卓)Jetpack-Paging使用
  9. Android(安卓)HandlerThread
  10. Android: MediaScanner生成thumbnail的算