1 背景
AMR(全稱是Adaptibve Multi-Rate)是一種音頻格式。由于其壓縮比比較大且質(zhì)量不錯的特性偎球,常常作為手機的音頻存儲的格式料滥。但是這個格式卻在跨平臺上表現(xiàn)非常差瞭空,大部分web都無法支持模软。由此經(jīng)常需要將AMR轉(zhuǎn)為MP3.
2 方案
名稱解釋:
- PCM: 一種音頻格式,能夠到底最高保真水平的厨钻。因此扼雏,PCM約定俗成了無損編碼,
- LAME: 目前最好的MP3編碼引擎,所謂編碼夯膀,即把未壓縮的音樂壓縮為mp3诗充。由于AMR已經(jīng)壓縮的格式,所以不能直接使用LAME轉(zhuǎn)為MP3诱建。
- FFmpeg: 一套可以用來記錄蝴蜓、轉(zhuǎn)換數(shù)字音頻、視頻俺猿,并能將其轉(zhuǎn)化為流的開源計算機程序茎匠。我們可以使用FFmpeg解碼AMR,將AMR轉(zhuǎn)為PCM押袍。
目前采用的方案是:通過FFmpeg將AMR轉(zhuǎn)為PCM, 通過LAME將PCM轉(zhuǎn)為MP3诵冒,已成功實現(xiàn)。
源碼: https://github.com/shike1116/amr2mp3
待解決問題:
- so較大谊惭,如果合入APK的包會增加8MB汽馋。\已經(jīng)精簡1.37MB add in 07/10
- 無法達(dá)到最理性的性能,由于中間多轉(zhuǎn)碼了一次圈盔,因此無法達(dá)到最理性的性能豹芯。
- 兼容性未知。
3 FFmpeg的編譯與使用
3.1 編譯環(huán)境的搭建
- 系統(tǒng)信息 :Ubuntu 16.04
- NDK :android-nkd-r9d
# 配置NDK環(huán)境變量
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文件進(jìn)行修改铁蹈。由于編譯出來的動態(tài)庫文件名的版本號在.so之后(例如“l(fā)ibavcodec.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)'
替換為下面內(nèi)容:
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的路徑姚建,根據(jù)自己的安裝位置進(jìn)行設(shè)置
NDK=/home/wangjf/ndk/android-ndk-r9d
# 編譯針對的平臺,可以根據(jù)自己的需求進(jìn)行設(shè)置
# 這里選擇最低支持android-14, arm架構(gòu)吱殉,生成的so庫是放在
# libs/armeabi文件夾下的掸冤,若針對x86架構(gòu)厘托,要選擇arch-x86
PLATFORM=$NDK/platforms/android-14/arch-arm
---
# 工具鏈的路徑,根據(jù)編譯的平臺不同而不同
# arm-linux-androideabi-4.9與上面設(shè)置的PLATFORM對應(yīng)稿湿,4.9為工具的版本號铅匹,
# 根據(jù)自己安裝的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 \ # 這個優(yōu)化其實是犧牲編碼解碼速度來換取動態(tài)庫的瘦身
--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
- 執(zhí)行編譯腳本
sudo ./build_android.sh
3.2.3 合入android工程
- 將android/arm/lib下的編譯好的.so文件以及android/arm/的include文件夾拷貝的android工程的jni目錄下
- 編寫轉(zhuǎn)碼的核心代碼
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;
}
//壓縮數(shù)據(jù)
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//解壓縮數(shù)據(jù)
AVFrame *frame = av_frame_alloc();
//frame->16bit 44100 PCM 統(tǒng)一音頻采樣格式與采樣率
SwrContext *swrContext = swr_alloc();
//音頻格式 重采樣設(shè)置參數(shù)
const enum AVSampleFormat in_sample = codecCtx->sample_fmt;//原音頻的采樣位數(shù)
//輸出采樣格式
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;
//設(shè)置音頻緩沖區(qū)間 16bit 44100 PCM數(shù)據(jù)
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編譯相關(guān)文件饺藤,包括 Android.mk 和 Application.mk
- 執(zhí)行ndk-build包斑,編譯so,以及對應(yīng)的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拷貝到j(luò)ni下
剔除不必要的文件目錄涕俗。例如i386這個目錄要刪除罗丰,還要刪除幾個非.h,.c作為擴展名的文件再姑,已經(jīng)Linux下的批處理文件萌抵,因為這些文件都是Android平臺下非必要的。
引入lame.h頭文件元镀。在LAME解壓目錄下找到include目錄绍填,將其下的lame.h頭文件拷貝到j(luò)ni目錄下。
引入lame.h頭文件栖疑。在LAME解壓目錄下找到include目錄沐兰,將其下的lame.h頭文件拷貝到j(luò)ni目錄下。
修改部分的源碼蔽挠,將部分?jǐn)?shù)據(jù)類型替換android支持的
4.2 編寫代碼
- 編寫轉(zhuǎn)碼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.設(shè)置lame mp3編碼的參數(shù)
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; // 當(dāng)前讀的wav文件的byte數(shù)目
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);
//把轉(zhuǎn)化后的mp3數(shù)據(jù)寫到文件里
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編譯相關(guān)文件住闯,包括 Android.mk 和 Application.mk
- 執(zhí)行ndk-build,編譯so澳淑,以及對應(yīng)的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 采樣率 不設(shè)置傳-1
* @param outSamplerate 采樣率 不設(shè)置傳-1
* @param numChannels 文件的聲道數(shù) 不設(shè)置傳-1
* @param brate 比特率 不設(shè)置傳-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");
}