1 背景
AMR(全称是Adaptibve Multi-Rate)是一种音频格式。由于其压缩比比较大且质量不错的特性,常常作为手机的音频存储的格式。但是这个格式却在跨平台上表现非常差,大部分web都无法支持。由此经常需要将AMR转为MP3.
2 方案
名称解释:
- PCM: 一种音频格式,能够到底最高保真水平的。因此,PCM约定俗成了无损编码,
- LAME: 目前最好的MP3编码引擎,所谓编码,即把未压缩的音乐压缩为mp3。由于AMR已经压缩的格式,所以不能直接使用LAME转为MP3。
- FFmpeg: 一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。我们可以使用FFmpeg解码AMR,将AMR转为PCM。
目前采用的方案是:通过FFmpeg将AMR转为PCM, 通过LAME将PCM转为MP3,已成功实现。
源码: https://github.com/shike1116/amr2mp3
待解决问题:
- so较大,如果合入APK的包会增加8MB。\已经精简1.37MB add in 07/10
- 无法达到最理性的性能,由于中间多转码了一次,因此无法达到最理性的性能。
- 兼容性未知。
3 FFmpeg的编译与使用
3.1 编译环境的搭建
- 系统信息 :Ubuntu 16.04
- NDK :android-nkd-r9d
# 配置NDK环境变量
gedit ~/.bashrc
export NDK_HOME=/home/wangjf/ndk/android-ndk-r9d
PATH=$NDK_HOME:$PATH
source ~/.bashrc
ndk-build
- FFmpeg版本 :FFmpeg3.0
3.2 编译脚本的编写
3.2.1 修改configure文件
下载FFmpeg源代码之后,首先需要对源代码中的configure文件进行修改。由于编译出来的动态库文件名的版本号在.so之后(例如“libavcodec.so.5.100.1”),而android平台不能识别这样文件名,所以需要修改这种文件名。
找到 -3.0/configure 文件,找到以下几行:
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)'
替换为下面内容:
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
3.2.2 编译脚本
- 新建脚本文件 ffmpeg-3.0/build_android.sh,保存下面脚本。
- 新建临时文件夹 ffmpeg-3.0/ffmpegtemp,将脚本中的 TMPDIR 改为自己的临时文件夹。
#!/bin/bash
# NDK的路径,根据自己的安装位置进行设置
NDK=/home/wangjf/ndk/android-ndk-r9d
# 编译针对的平台,可以根据自己的需求进行设置
# 这里选择最低支持android-14, arm架构,生成的so库是放在
# libs/armeabi文件夹下的,若针对x86架构,要选择arch-x86
PLATFORM=$NDK/platforms/android-14/arch-arm
---
# 工具链的路径,根据编译的平台不同而不同
# arm-linux-androideabi-4.9与上面设置的PLATFORM对应,4.9为工具的版本号,
# 根据自己安装的NDK版本来确定,一般使用最新的版本
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
ARCH=arm
TARGETOS=android
PREFIX=$(pwd)/$TARGETOS/$ARCH
ADDITIONAL_CONFIGURE_FLAG=
./configure \
--prefix=$PREFIX \
--enable-shared \333waawawawawa长度cd
--disable-static \
--disable-doc \
--disable-programs \
--enable-small \ # 这个优化其实是牺牲编码解码速度来换取动态库的瘦身
--disable-avdevice \
--disable-devices \
--disable-protocols \
--enable-protocol=file \
--enable-cross-compile \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--sysroot=$PLATFORM \
--extra-cflags="-Os -fpic" \
--extra-ldflags="$ADDI_LDFLAGS" \
--arch="$ARCH" \
--target-os="$TARGETOS"
make clean
make
make install
- 执行编译脚本
sudo ./build_android.sh
3.2.3 合入android工程
- 将android/arm/lib下的编译好的.so文件以及android/arm/的include文件夹拷贝的android工程的jni目录下
- 编写转码的核心代码
JNIEXPORT void JNICALL Java_com_sangfor_pocket_utils_FFmpegUtil_jniRun
(JNIEnv * env, jclass cls,
jstring jinput, jstring joutput){
char* input = Jstring2CStr(env,jinput) ;
char* output = Jstring2CStr(env,joutput);
av_register_all();
AVFormatContext *pFormatCtx = avformat_alloc_context();
//打开音频文件
int resultint = avformat_open_input(&pFormatCtx, input, NULL, NULL);
if (resultint != 0) {
LOGI("%s", "open avformat fail");
LOGE(" resultint %d", resultint);
return;
}
//获取输入文件信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
LOGI("%s", "open stream info fail");
return;
}
//获取音频流索引位置
int i = 0, audio_stream_idx = -1;
for (; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_stream_idx = i;
break;
}
}
//获取解码器
AVCodecContext *codecCtx = pFormatCtx->streams[audio_stream_idx]->codec;
AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
//打开解码器
if (avcodec_open2(codecCtx, codec, NULL) < 0) {
LOGI("%s", "open avcodec fial");
return;
}
//压缩数据
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//解压缩数据
AVFrame *frame = av_frame_alloc();
//frame->16bit 44100 PCM 统一音频采样格式与采样率
SwrContext *swrContext = swr_alloc();
//音频格式 重采样设置参数
const enum AVSampleFormat in_sample = codecCtx->sample_fmt;//原音频的采样位数
//输出采样格式
const enum AVSampleFormat out_sample = AV_SAMPLE_FMT_S16;//16位
int in_sample_rate = codecCtx->sample_rate;// 输入采样率
int out_sample_rate = 16000;//输出采样
//输入声道布局
uint64_t in_ch_layout = codecCtx->channel_layout;
//输出声道布局
uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;//2通道 立体声 AV_CH_LAYOUT_STEREO AV_CH_LAYOUT_MONO
/**
* struct SwrContext *swr_alloc_set_opts(struct SwrContext *s,
int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate,
int64_t in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate,
int log_offset, void *log_ctx);
*/
swr_alloc_set_opts(swrContext, out_ch_layout, out_sample, out_sample_rate, in_ch_layout, in_sample,
in_sample_rate, 0, NULL);
swr_init(swrContext);
int got_frame = 0;
int ret;
int out_channerl_nb = av_get_channel_layout_nb_channels(out_ch_layout);
LOGE("out_channerl_nb %d ", out_channerl_nb);
int count = 0;
//设置音频缓冲区间 16bit 44100 PCM数据
uint8_t *out_buffer = (uint8_t *) av_malloc(2 * 44100);
FILE *fp_pcm = fopen(output, "wb");//输出到文件
while (av_read_frame(pFormatCtx, packet) >= 0) {
ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, packet);
LOGE("decode ing %d", count++);
if (ret < 0) {
LOGE("decode finish");
}
//解码一帧
if (got_frame > 0) {
/**
* int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);
*/
swr_convert(swrContext, &out_buffer, 2 * 44100,
(const uint8_t **) frame->data, frame->nb_samples);
/**
* int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
enum AVSampleFormat sample_fmt, int align);
*/
int out_buffer_size = av_samples_get_buffer_size(NULL, out_channerl_nb, frame->nb_samples,
out_sample, 1);
fwrite(out_buffer, 1, out_buffer_size, fp_pcm);//输出到文件
}
}
fclose(fp_pcm);
av_frame_free(&frame);
av_free(out_buffer);
swr_free(&swrContext);
avcodec_close(codecCtx);
avformat_close_input(&pFormatCtx);
}
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;
}
- 配置jni编译相关文件,包括 Android.mk 和 Application.mk
- 执行ndk-build,编译so,以及对应的java代码
public class FFmpegUtil {
public static int run(String wavPath,String mp3Path){
return jniRun(wavPath,mp3Path);
}
static native int jniRun(String wavPath,String mp3Path);
static{
System.loadLibrary("avutil");
System.loadLibrary("swresample");
System.loadLibrary("avcodec");
System.loadLibrary("avformat");
System.loadLibrary("swscale");
System.loadLibrary("avfilter");
System.loadLibrary("avdevice");
System.loadLibrary("ffmpeg");
}
}
public void test(){
FFmpegUtil.run("/storage/emulated/0/test/a1.amr","/storage/emulated/0/test/a13.pcm");
}
4 LAME的编译与使用
4.1 引入lame
下载源码
LAME主页:http://lame.sourceforge.net/
LAME源码:http://sourceforge.net/projects/lame/files/lame/3.99/将libmp3lame拷贝到jni下
剔除不必要的文件目录。例如i386这个目录要删除,还要删除几个非.h,.c作为扩展名的文件,已经Linux下的批处理文件,因为这些文件都是Android平台下非必要的。
引入lame.h头文件。在LAME解压目录下找到include目录,将其下的lame.h头文件拷贝到jni目录下。
引入lame.h头文件。在LAME解压目录下找到include目录,将其下的lame.h头文件拷贝到jni目录下。
修改部分的源码,将部分数据类型替换android支持的
4.2 编写代码
- 编写转码c代码
JNIEXPORT void JNICALL Java_com_sangfor_pocket_appservice_callrecord_utils_LameUtil_jniConvertmp3
(JNIEnv * env, jclass cls ,
jstring jwav, jstring jmp3,
jint inSamplerate, jint outSamplerate, jint numChannels, jint brate, jint quality, jint vbrModel){
char* cwav = Jstring2CStr(env,jwav) ;
char* cmp3 = Jstring2CStr(env,jmp3);
//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编码的参数
if(inSamplerate >= 0){
lame_set_in_samplerate(lame , inSamplerate);
}
if(outSamplerate >= 0){
lame_set_out_samplerate(lame, outSamplerate);
}
if(numChannels >= 0){
lame_set_num_channels(lame, numChannels);
}
if(brate >= 0){
lame_set_brate(lame, brate);
}
if(quality >= 0){
lame_set_quality(lame, quality);
}
if(vbrModel >= 0){
switch (vbrModel) {
case 0:
lame_set_VBR(lame, vbr_default);
break;
case 1:
lame_set_VBR(lame, vbr_off);
break;
case 2:
lame_set_VBR(lame, vbr_abr);
break;
case 3:
lame_set_VBR(lame, vbr_mtrh);
break;
default:
break;
}
}
lame_init_params(lame);
//3.开始写入
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;
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);
lame_mp3_tags_fid(lame, fmp3);
lame_close(lame);
fclose(fwav);
fclose(fmp3);
}
- 配置jni编译相关文件,包括 Android.mk 和 Application.mk
- 执行ndk-build,编译so,以及对应的java代码
public class LameUtil {
public static int run(String wav,String mp3){
return jniConvertmp3(wav, mp3, 16000,-1,2,-1,5,1);
}
/**
* @param wavPath wav路径
* @param mp3Path MP3 路径
* @param inSamplerate 采样率 不设置传-1
* @param outSamplerate 采样率 不设置传-1
* @param numChannels 文件的声道数 不设置传-1
* @param brate 比特率 不设置传-1
* @param quality 0-9 2=high 5 = medium 7=low
* @param vbrModel 0 = vbr_default 1 = vbr_off 2 = vbr_abr 3 = vbr_mtrh
*
* 可参考 https://blog.csdn.net/xjwangliang/article/details/7065985
* @return
*/
static native int jniConvertmp3(String wavPath,String mp3Path,int inSamplerate, int outSamplerate, int numChannels, int brate, int quality, int vbrModel);
static{
System.loadLibrary("lame");
}
}
public void test(){
LameUtil.run("/storage/emulated/0/test/a13.pcm", "/storage/emulated/0/test/a13.mp3");
}