客户端AMR转码MP3(一)

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文件

  1. 下载FFmpeg源代码之后,首先需要对源代码中的configure文件进行修改。由于编译出来的动态库文件名的版本号在.so之后(例如“libavcodec.so.5.100.1”),而android平台不能识别这样文件名,所以需要修改这种文件名。

  2. 找到 -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 编译脚本

  1. 新建脚本文件 ffmpeg-3.0/build_android.sh,保存下面脚本。
  2. 新建临时文件夹 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

  1. 执行编译脚本
sudo ./build_android.sh

3.2.3 合入android工程

  1. 将android/arm/lib下的编译好的.so文件以及android/arm/的include文件夹拷贝的android工程的jni目录下
  2. 编写转码的核心代码
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;
}
  1. 配置jni编译相关文件,包括 Android.mk 和 Application.mk
  2. 执行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

  1. 下载源码
    LAME主页:http://lame.sourceforge.net/
    LAME源码:http://sourceforge.net/projects/lame/files/lame/3.99/

  2. 将libmp3lame拷贝到jni下

  3. 剔除不必要的文件目录。例如i386这个目录要删除,还要删除几个非.h,.c作为扩展名的文件,已经Linux下的批处理文件,因为这些文件都是Android平台下非必要的。

  4. 引入lame.h头文件。在LAME解压目录下找到include目录,将其下的lame.h头文件拷贝到jni目录下。

  5. 引入lame.h头文件。在LAME解压目录下找到include目录,将其下的lame.h头文件拷贝到jni目录下。

  6. 修改部分的源码,将部分数据类型替换android支持的

4.2 编写代码

  1. 编写转码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);
}
  1. 配置jni编译相关文件,包括 Android.mk 和 Application.mk
  2. 执行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");
}

推荐阅读更多精彩内容