客戶端AMR轉(zhuǎn)碼MP3(一)

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

  1. 下載FFmpeg源代碼之后驱敲,首先需要對源代碼中的configure文件進(jìn)行修改铁蹈。由于編譯出來的動態(tài)庫文件名的版本號在.so之后(例如“l(fā)ibavcodec.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)'

替換為下面內(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 編譯腳本

  1. 新建腳本文件 ffmpeg-3.0/build_android.sh围辙,保存下面腳本。
  2. 新建臨時文件夾 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

  1. 執(zhí)行編譯腳本
sudo ./build_android.sh

3.2.3 合入android工程

  1. 將android/arm/lib下的編譯好的.so文件以及android/arm/的include文件夾拷貝的android工程的jni目錄下
  2. 編寫轉(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;
}
  1. 配置jni編譯相關(guān)文件饺藤,包括 Android.mk 和 Application.mk
  2. 執(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

  1. 下載源碼
    LAME主頁:http://lame.sourceforge.net/
    LAME源碼:http://sourceforge.net/projects/lame/files/lame/3.99/

  2. 將libmp3lame拷貝到j(luò)ni下

  3. 剔除不必要的文件目錄涕俗。例如i386這個目錄要刪除罗丰,還要刪除幾個非.h,.c作為擴展名的文件再姑,已經(jīng)Linux下的批處理文件萌抵,因為這些文件都是Android平臺下非必要的。

  4. 引入lame.h頭文件元镀。在LAME解壓目錄下找到include目錄绍填,將其下的lame.h頭文件拷貝到j(luò)ni目錄下。

  5. 引入lame.h頭文件栖疑。在LAME解壓目錄下找到include目錄沐兰,將其下的lame.h頭文件拷貝到j(luò)ni目錄下。

  6. 修改部分的源碼蔽挠,將部分?jǐn)?shù)據(jù)類型替換android支持的

4.2 編寫代碼

  1. 編寫轉(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);
}
  1. 配置jni編譯相關(guān)文件住闯,包括 Android.mk 和 Application.mk
  2. 執(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");
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末比原,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子杠巡,更是在濱河造成了極大的恐慌量窘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氢拥,死亡現(xiàn)場離奇詭異蚌铜,居然都是意外死亡,警方通過查閱死者的電腦和手機嫩海,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門冬殃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人叁怪,你說我怎么就攤上這事审葬。” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵涣觉,是天一觀的道長痴荐。 經(jīng)常有香客問我,道長官册,這世上最難降的妖魔是什么生兆? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮膝宁,結(jié)果婚禮上皂贩,老公的妹妹穿的比我還像新娘。我一直安慰自己昆汹,他們只是感情好明刷,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著满粗,像睡著了一般辈末。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上映皆,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天挤聘,我揣著相機與錄音,去河邊找鬼捅彻。 笑死组去,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的步淹。 我是一名探鬼主播从隆,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缭裆!你這毒婦竟也來了键闺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤澈驼,失蹤者是張志新(化名)和其女友劉穎辛燥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缝其,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡挎塌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了内边。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榴都。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖假残,靈堂內(nèi)的尸體忽然破棺而出缭贡,到底是詐尸還是另有隱情,我是刑警寧澤辉懒,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布阳惹,位于F島的核電站,受9級特大地震影響眶俩,放射性物質(zhì)發(fā)生泄漏莹汤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一颠印、第九天 我趴在偏房一處隱蔽的房頂上張望纲岭。 院中可真熱鬧,春花似錦线罕、人聲如沸止潮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喇闸。三九已至,卻和暖如春询件,著一層夾襖步出監(jiān)牢的瞬間燃乍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工宛琅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留刻蟹,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓嘿辟,卻偏偏與公主長得像舆瘪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子红伦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容