Android NDK之Lame初探

之前在做车载语音微信项目的时候,基于网页版微信原理,在发送语音消息时先将录音消息传到讯飞的车载服务服务器,然后获取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对象。






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值