之前在做车载语音微信项目的时候,基于网页版微信原理,在发送语音消息时先将录音消息传到讯飞的车载服务服务器,然后获取URL,只发送URL就可以了。由于录音数据为pcm格式,pcm转wav比较容易,直接加个头文件就可以了,但是wav转mp3呢,在这里我是用了比较出名的MP3 Encoder库Lame,正好通过Lame库的使用熟悉Android NDK开发:
1.首先介绍一下Lame库,LAME是目前最好的MP3编码引擎。LAME编码出来的MP3音色纯厚、空间宽广、低音清晰、细节表现良好,它独创的心理音响模型技术保证了CD音频还原的真实性,配合VBR和ABR参数,音质几乎可以媲美CD音频,但文件体积却非常小。对于一个免费引擎,LAME的优势不言而喻。关于LAME的介绍可以在百度百科,维基百科中找到,我在这里不再赘述了,但是要知道LAME可以帮助我们将wav无损音频文件转码成mp3这种体积相对较小的音频格式文件,哈哈,看了这么多是不是想睡觉了,别急,下面上干货。
2.LAME的源码是托管到sourceforge.net上的,我们开发一个基于LAME的项目,就不得不下载其源码用于编译。获取Lame库源码,戳这里:
LAME主页:http://lame.sourceforge.net/
LAME下载:https://sourceforge.net/projects/lame/files/lame/3.99/
3.OK,在这里我将音频格式转化的操作封装为一个工具类RecordUtil,在这里面实现了pcm转wav以及wav转mp3,pcm转wav很容易,加个头文件就可以了。wav转mp3,使用native方法convertmp3(String wav,String mp3),该native方法将从Java层获取到的两个音频文件的路径传递给C端,C端拿到这两个路径,就可以进行读写和编解码操作了。源代码如下:
public class RecordUtil {
private static final int SAMPLE_RATE_IN_HZ = 8000;
private MediaRecorder recorder = new MediaRecorder();
// 录音的路径
private String mPath;
private Handler mHandler;
static {
System.loadLibrary("Hello");
}
public native void convertmp3(String wav, String mp3);
public RecordUtil(String path, Handler handler) {
mPath = path;
this.mHandler = handler;
}
/**
* 将pcm格式的文件加头处理成wav格式的音频文件
*/
public void pcm2wav(String pcmFilePath, String wavFilePath) {
Pcm2Wav tool = new Pcm2Wav();
try {
FileInputStream fis = new FileInputStream(pcmFilePath);
FileOutputStream fos = new FileOutputStream(wavFilePath);
byte[] buf = new byte[1024 * 4];
int size = fis.read(buf);
int PCMSize = 0;
while (size != -1) {
PCMSize += size;
size = fis.read(buf);
}
fis.close();
// 添加wav头
WaveHeader header = new WaveHeader();
header.fileLength = PCMSize + (44 - 8);
header.FmtHdrLeth = 16;
header.BitsPerSample = 16;
header.Channels = 1;
header.FormatTag = 0x0001;
header.SamplesPerSec = 16000;
header.BlockAlign = (short) (header.Channels * header.BitsPerSample / 8);
header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
header.DataHdrLeth = PCMSize;
byte[] h = header.getHeader();
assert h.length == 44;
// write header
fos.write(h, 0, h.length);
// 将pcm文件数据先读到buf,再从buf写到wav文件中
fis = new FileInputStream(pcmFilePath);
size = fis.read(buf);
while (size != -1) {
fos.write(buf, 0, size);
size = fis.read(buf);
}
fis.close();
fos.close();
System.out.println("Convert OK!");
} catch (Exception e) {
Log.e("RecordUtil",
"pcm failed to convert into wav File:" + e.getMessage());
}
}
public void deleteFile(String path) {
File file = new File(path);
if (file.exists()) {
file.delete();
}
}
/**
* wav格式转化成MP3格式
*
* @param wavFileName
*/
public void wav2mp3(final String wavFileName) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
convertmp3(wavFileName, wavFileName.replace(".wav", ".mp3"));
// wav转换为MP3后通知应用上传MP3资源
mHandler.sendEmptyMessage(2);
}
});
thread.start();
}
}
</span>
4.将目录切换到工程目录下的src目录,执行如下的javah命令:
javah com.iflytek.wechat.common.utils.voice.RecordUtil
会生成一个jni接口映射C头文件,文件名格式为包名_类名,而生成的与JNI接口方法映射的JNI函数名为Java_包名_类名_JNI接口方法名,代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_iflytek_wechat_common_utils_voice_RecordUtil */
#ifndef _Included_com_iflytek_wechat_common_utils_voice_RecordUtil
#define _Included_com_iflytek_wechat_common_utils_voice_RecordUtil
#ifdef __cplusplus
extern "C" {
#endif
#undef com_iflytek_wechat_common_utils_voice_RecordUtil_SAMPLE_RATE_IN_HZ
#define com_iflytek_wechat_common_utils_voice_RecordUtil_SAMPLE_RATE_IN_HZ 8000L
/*
* Class: com_iflytek_wechat_common_utils_voice_RecordUtil
* Method: convertmp3
* Signature: (Ljava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_iflytek_wechat_common_utils_voice_RecordUtil_convertmp3
(JNIEnv *, jobject, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif</span>
在工程目录下新建一个jni的目录,将生成的JNI接口映射C头文件copy到jni目录中。之后将下载来的LAME源码解压到本地,打开解压后的目录,找到libmp3lame目录,将该目录下的所有的文件都拷贝到jni目录下;剔除不必要的文件目录。例如i386这个目录要删除,还要删除几个非.h,.c作为扩展名的文件,已经Linux下的批处理文件,因为这些文件都是Android平台下非必要的;引入lame.h头文件。在LAME解压目录下找到include目录,将其下的lame.h头文件拷贝到jni目录下,如果这个目录没有被引入,会报如下的错误:
修改util.h的源码。在JNI目录下找到util.h文件,在574行找到ieee754_float32_t数据类型,将其修改为float类型,ieee754_float32_t是Linux或者是Unix下支持的数据类型,在Android下并不支持。如果不修改,则编译源码的时候会报如下错误:
5.接下来就是重头戏,创建一个C源文件,取名Hello.c,在其中实现JNI接口映射C头文件中的JNI函数,完成具体的wav转mp3操作,直接上代码:
#include <jni.h>
#include <stdio.h>
#include "com_iflytek_wechat_common_utils_voice_RecordUtil.h"
#include <android/log.h>
#include <lame.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
int flag = 0;
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env, "GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
"(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray) (*env)->CallObjectMethod(env, jstr, mid,
strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr);
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;
}
JNIEXPORT void Java_com_iflytek_wechat_common_utils_voice_RecordUtil_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);
//打开 wav,MP3文件
FILE* fwav = fopen(cwav, "rb");
FILE* fmp3 = fopen(cmp3, "wb");
short int wav_buffer[8192 * 2];
unsigned char mp3_buffer[8192];
//初始化lame的编码器
lame_t lame = lame_init();
//设置lame mp3编码的采样率
lame_set_in_samplerate(lame, 44100);
lame_set_num_channels(lame, 2);
//设置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);
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);
}
</span>
6.最后就是编写配置文件以及交叉编译了。首先看看Android.mk文件,这个就麻烦了,因为上面我们将LAME整个源码文件全部拷贝到jni目录下了,这些文件都是要重新编译的,所以我们需要在Android.mk文件的LOCAL_SRC_FILES这个字段上,将所有的源码文件都要配置上去,不仅包括我们自定义的c文件,更有LAME中的c源码文件,由于LAME包含的文件太多了,所以在编写LOCAL_SRC_FILES时要格外小心,写错一个就有可能编译不通过。下面是我的Android.mk文件内容:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Hello
LOCAL_SRC_FILES := 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.c Hello.c
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)</span>
Application.mk文件内容:
APP_ABI := all</span>
如果Eclipse已经配置好了NDK开发环境,此时直接Build一下Project就可以看到在libs下生成了相应的so文件,此时就可以在Android中使用Lame库实现wav转mp3了,如果需要使用lame库中其他的功能,也可以按照类似的方法去操作。JNI相当于在Java与C/C++之间形成了一个映射,在Java中可以通过Native方法调用C/C++中的代码,在C/C++ JNI接口映射函数中也可以通过JNIEnv来调用JNI函数访问Java虚拟机,进而操作Java对象。