美文网首页程序员
客户端AMR转码MP3(一)

客户端AMR转码MP3(一)

作者: 毛x脑内博览会 | 来源:发表于2018-07-08 13:17 被阅读0次

    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");
    }
    
    

    相关文章

      网友评论

        本文标题:客户端AMR转码MP3(一)

        本文链接:https://www.haomeiwen.com/subject/dvlouftx.html