前言
此处将使用CmakeList方式对Lame库进行编译移植到Android上面使用,网上很多文章都是使用两个文件方式转码,那种不符合即用即转,故此处使用实时流的方式。
试验条件
- windows11
- Android Studio Hedgehog
- Cmake 3.22.1
- NDK 25.1.8937393
- SDK 34
# MP3转PCM需要的文件(mpglib文件夹)
libmp3lame/common.c
libmp3lame/common.h
libmp3lame/huffman.h
libmp3lame/interface.c
libmp3lame/interface.h
libmp3lame/l2tables.h
libmp3lame/layer1.c
libmp3lame/layer1.h
libmp3lame/layer2.c
libmp3lame/layer2.h
libmp3lame/layer3.c
libmp3lame/layer3.h
libmp3lame/mpg123.h
libmp3lame/mpglib.h
libmp3lame/tabinit.c
libmp3lame/tabinit.h
libmp3lame/dct64_i386.c
libmp3lame/dct64_i386.h
libmp3lame/decode_i386.c
libmp3lame/decode_i386.h
# PCM转MP3需要的文件
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
步骤:
-
下载Lame文件
此处以3.10为例,去官网下载.tar.gz文件对其进行解压获得文件夹lame-3.100 - 代码集成
先建一个nativeLib
工程
➪ 在main/cpp
文件夹下建立文件夹libmp3lame
➪ 将解压的/lame-3.100/libmp3lame/
文件夹下的所有文件(文件夹忽略)复制到工程main/cpp/libmp3lame
文件夹中
➪ 将lame-3.100\include
文件夹下的lame.h
文件也复制到libmp3lame
文件夹中
➪ 在CmakeList.txt文件中登记libmp3lame中的所有.c
文件
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
lame-lib.cpp
# PCM转MP3需要的文件
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)
➪ 在app的build.gradle文件下添加C或C++编译引导,镇压cpp文件代码报错
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"//指导CPP编译
cFlags "-DSTDC_HEADERS"//声明使用标准头文件,避免C库版本不同编译报错
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
➪ 修改文件里的报错
1.删除 fft.c 文件的 47 行的 include“vector/lame_intrin.h”
2.修改 set_get.h 文件的 24 行的 #include“lame.h”
3.将 util.h 文件的 570 行的”extern ieee754_float32_t fast_log2(ieee754_float32_t x);” 替换为 “extern float fast_log2(float x);”
➪ 修改lame.c
文件里的方法报错
lame_encode_buffer_interleaved
lame_encode_buffer
lame_encode_flush
将lame.c里的方法入参类型unsign变为sign,二进制表示为补码的意思
➪ 开始具体做
- 定义native方法,
LameEncode.java
/**
* 调用libLame_mp3.so中的native方法
*/
public class LameEncode {
/**
* 初始化lame,cpp中初始化采用lame默认参数配置
* @param sampleRate :采样率 -- 录音默认44100
* @param channelCount :通道数 -- 录音默认双通道2
* @param audioFormatBit :位宽 -- 录音默认ENCODING_PCM_16BIT 16bit
* @param quality :MP3音频质量 0~9 其中0是最好,非常慢,9是最差 2=high(高) 5 = medium(中) 7=low(低)
*/
public static native void init(int sampleRate, int channelCount, int audioFormatBit, int quality);
/**
* 启用lame编码
*
* @param pcmBuffer :音频数据源
* @param mp3_buffer :写入MP3数据buffer
* @param sample_num :采样个数
*/
public static native int encoder(short[] pcmBuffer, byte[] mp3_buffer, int sample_num);
/**
* 刷新缓冲器
*
* @param mp3_buffer :MP3编码buffer
* @return int 返回剩余编码器字节数据,需要写入文件
*/
public static native int flush(byte[] mp3_buffer);
/**
* 释放编码器
*/
public static native void close();
/**
* 写入mp3标记,标记要不要都行
*/
public static native void writeMetaTag(String mp3FilePath);
/**
* 获取字节序;http网络字节序使用的是大端序(因为其默认先发送高位接收端先收到高位并被存入低地址)、java默认大端序
* ,C语言与计算机的内部处理都是小端字节序
* @return
*/
public static native int getEndian();
}
lame-lib.cpp
#include <stdio.h>
#include <cstdlib>
#include <jni.h>
#include <sys/stat.h>
#include <iosfwd>
#include <fstream>
#include "libmp3lame/lame.h"
//lame 对象
static lame_global_flags *gfp = NULL;
//jstring转string -- defined in lame_util.c 69 lines
char *Jstring2CStr(JNIEnv *env, jstring jstr) {
char *rtn = NULL;
jclass clsstring = env->FindClass("java/lang/String"); //String
jstring strencode = env->NewStringUTF("GB2312"); // 得到一个java字符串 "GB2312"
jmethodID mid = env->GetMethodID(clsstring, "getBytes",
"(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid,
strencode); // String .getByte("GB2312");
jsize alen = env->GetArrayLength(barr); // byte数组的长度
jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
rtn = (char *) malloc(alen + 1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0); //
return rtn;
}
//初始化lame参数
extern "C" JNIEXPORT void JNICALL
Java_com_xyzl_android_lib_1lamemp3_LameEncode_init(JNIEnv *env, jclass jclass1, jint sampleRate,
jint channelCount,
jint audioFormatBit, jint quality) {
//初始化lame -- 采用默认输入音频参数配置,转换为MP3后,bitrate比特率为128kbps
gfp = lame_init();
//采样率
lame_set_in_samplerate(gfp, sampleRate);
//声道数
lame_set_num_channels(gfp, channelCount);
//输入采样率
lame_set_out_samplerate(gfp, sampleRate);
//位宽
lame_set_brate(gfp, audioFormatBit);
//音频质量
lame_set_quality(gfp, quality);
//初始化参数配置
lame_init_params(gfp);
}
//开启MP3编码
extern "C" JNIEXPORT jint JNICALL
Java_com_xyzl_android_lib_1lamemp3_LameEncode_encoder(JNIEnv *env, jclass jclass1, jshortArray pcm_buffer, jbyteArray mp3_buffer, jint sample_num) {
//lame转换需要short指针参数
jshort *pcm_buf = env->GetShortArrayElements(pcm_buffer, JNI_FALSE);
//获取MP3数组长度
const jsize mp3_buff_len = env->GetArrayLength(mp3_buffer);
//获取buffer指针
jbyte *mp3_buf = env->GetByteArrayElements(mp3_buffer, JNI_FALSE);
//编译后得bytes
int encode_result;
//根据输入音频声道数判断
if (lame_get_num_channels(gfp) == 2) {
encode_result = lame_encode_buffer_interleaved(gfp, pcm_buf, sample_num / 2, mp3_buf,mp3_buff_len);
} else {
encode_result = lame_encode_buffer(gfp, pcm_buf, pcm_buf, sample_num, mp3_buf,mp3_buff_len);
}
//释放资源
env->ReleaseShortArrayElements(pcm_buffer, pcm_buf, 0);
env->ReleaseByteArrayElements(mp3_buffer, mp3_buf, 0);
return encode_result;
}
//关闭MP3编码buffer
extern "C" JNIEXPORT jint JNICALL
Java_com_xyzl_android_lib_1lamemp3_LameEncode_flush(JNIEnv *env, jclass jclass1, jbyteArray mp3_buffer) {
//获取MP3数组长度
const jsize mp3_buff_len = env->GetArrayLength(mp3_buffer);
//获取buffer指针
jbyte *mp3_buf = env->GetByteArrayElements(mp3_buffer, JNI_FALSE);
//刷新编码器缓冲,获取残留在编码器缓冲里的数据
int flush_result = lame_encode_flush(gfp, mp3_buf, mp3_buff_len);
env->ReleaseByteArrayElements(mp3_buffer, mp3_buf, 0);
return flush_result;
}
std::string jstring2str(JNIEnv *env, jstring jstr) {
const char *c_str = env->GetStringUTFChars(jstr, nullptr);
std::string result(c_str);
env->ReleaseStringUTFChars(jstr, c_str);
return result;
}
// 写入MP3的Tag
extern "C" void JNICALL
Java_com_xyzl_android_lib_1lamemp3_LameEncode_writeMetaTag(JNIEnv *env, jclass clazz, jstring mp3FilePath) {
FILE *mp3File = fopen(jstring2str(env, mp3FilePath).c_str(), "ab+");
lame_mp3_tags_fid(gfp, mp3File);
fclose(mp3File);
}
//释放编码器
extern "C" JNIEXPORT void JNICALL
Java_com_xyzl_android_lib_1lamemp3_LameEncode_close(JNIEnv *env, jclass type) {
lame_close(gfp);
}
union
{
char ch;
int i;
}un;
extern "C"
JNIEXPORT jint JNICALL
Java_com_xyzl_android_lib_1lamemp3_LameEncode_getEndian(JNIEnv *env, jclass clazz) {
un.i = 0x12345678;
if(un.ch == 0x12) {
printf("big endian\n");
return 1;
} else {
printf("small endain\n");
return 0;
}
}
使用顺序为init
、encode
、flush
、writeMetaTag
、close
;
- init()录音初始化
//初始化录音缓冲大小
int bufferSize = AudioRecord.getMinBufferSize(16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
//初始化录音抓取
AudioRecord mAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, 16000,AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
/**
* 初始化转码
*
* @param activity
* @param bufferSize
*/
private void initLameMp3(Activity activity, int bufferSize) {
System.loadLibrary("Lame_mp3");
short channelCount = (short) (VoiceParams.CHANNEL_CONFIG == AudioFormat.CHANNEL_IN_MONO ? 1 : 2); // 通道数
//lame编码器中的MP3——buffer计算公式,定义在lame.h头文件中
mp3_buff = new byte[(int) ((int) (7200 + (bufferSize * 2 * 1.25 * 2)))];
//单声道,16位,采样率16KHz 与录音抓取参数保持一致
LameEncode.init(VoiceParams.SAMPLE_RATE, channelCount, 16, 7);
String filePath = FileUtils.getFilePath(activity);
initFileStream(filePath);
}
/**
* 初始化文件输出流
*
* @param filePath :文件路径
*/
private void initFileStream(String filePath) {
try {
mp3FileOutputStream = new FileOutputStream(filePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
mp3FileOutputStream = null;
}
}
-
encode()实时转码
由于本次转码功能还涉及到阿里的语音转文字,经实验证明若不使用其方法入参byte[] buffer
去AudioRecord里读取缓存,语音就无法转文字,又由于Lame转码需要的pcm流是short[]
,故需要对byte[] buffer
进行转码,
@Override
public int onNuiNeedAudioData(byte[] buffer, int len) {
int ret = 0;
if (mAudioRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "audio recorder not init");
return -1;
}
ret = mAudioRecorder.read(buffer, 0, len);
short[] pcmBuffer = ByteUtil.audioBytesToShort(buffer.clone());
convertMp3(pcmBuffer,pcmBuffer.length);
return ret;
}
在转码过程中遇到数据播放只有雪花噪声的问题,毫无疑问文件是坏的,故怀疑字节序问题,试出如下方法,转成short[]
后使用encoder()
转码拿到mp3流
/**
* 音频字节 8 bit 转为 16 bit 的 short 类型,因为L、R声道
*
* @param byteArray
* @return
*/
public static short[] audioBytesToShort(byte[] byteArray) {
ShortBuffer shortBuffer = ByteBuffer.wrap(byteArray).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] shortArray = new short[shortBuffer.capacity()];
shortBuffer.get(shortArray);
return shortArray;
}
-
writeFlush()
在录音流关闭后,最后一次对encoder所用的转码缓冲流作出清理并返回剩余数据
/**
* 回写lame缓冲区剩余字节数据
*/
private void writeFlush() {
int flushResult = LameEncode.flush(mp3_buff);
if (flushResult > 0) {
try {
mp3FileOutputStream.write(mp3_buff, 0, flushResult);
mp3FileOutputStream.close();
mp3FileOutputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
writeMetaTag()
在mp3FileOutputStream关闭后,调用写标记的方法对mp3文件刻上标记,经Audacity、Beyond Compare 4对比查看由于lame.c
文件里lame_mp3_tags_fid
方法这段cfg = &gfc->cfg; if (!cfg->write_lame_tag) { return; }
这段代码返回了,所以实际并没有写入mp3相关标准的标记,个人认为这个标准标记不写也成
LameEncode.writeMetaTag(mp3FilePath);
-
close()
正常调用jni方法关闭
中间遇到使用MediaPlayer无法播放的问题,原因是因为设置属性的时候多设置了一个setFlags,把这个.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
去掉就好了
mediaPlayer.setAudioAttributes(
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
最后感谢博客园
让我看到了一堆免费资料,一模一样的内容在CSDN居然是收费文章,究竟是不是它爬的不得而知。
本作参考源码