手把手教你Android如何使用NDK實(shí)現(xiàn)一個MP3轉(zhuǎn)碼庫

通過本文你可以學(xué)到以下知識:

  • 如何實(shí)現(xiàn)一個Android MP3轉(zhuǎn)碼庫
  • 一些和音頻轉(zhuǎn)碼相關(guān)的基礎(chǔ)知識
  • 如何使用NDK將c/c++項(xiàng)目移植到Android端戈擒,并使用Java調(diào)用c/c++代碼
  • 如何使用CMake構(gòu)建NDK項(xiàng)目
  • 如何生成不同CPU架構(gòu)所需的動態(tài)鏈接庫

工具簡介

Lame

LAME 是最好的MP3編碼器靶擦,速度快粗卜,效果好梁棠,特別是中高碼率和VBR編碼方面构眯。

NDK

原生開發(fā)工具包糜值,即幫助開發(fā)原生代碼的一系列工具个榕,包括但不限于編譯工具凯旭、一些公共庫概耻、開發(fā)IDE等。它提供了完整的一套將 c/c++ 代碼編譯成靜態(tài)/動態(tài)庫的工具罐呼,而 Android.mkApplication.mk 你可以認(rèn)為是描述編譯參數(shù)和一些配置的文件鞠柄。比如指定使用c++11還是c++14編譯,會引用哪些共享庫嫉柴,并描述關(guān)系等厌杜,還會指定編譯的abi。只有有了這些 NDK 中的編譯工具才能準(zhǔn)確的編譯 c/c++ 代碼计螺。

CMake簡介

CMake是一個跨平臺的編譯工具夯尽,它并不會直接編譯出對象,而是根據(jù)自定義的語言規(guī)則(CMakeLists.txt)生成 對應(yīng) makefile 或 project 文件登馒,然后再調(diào)用底層的編譯匙握。Android Studio 2.2以后開始支持CMake,所以現(xiàn)在我們有2種方式來編譯c/c++ 代碼标沪。一個是 ndk-build + Android.mk + Application.mk 組合岔绸,另一個是 CMake + CMakeLists.txt 組合了讨,它們都不會影響我們的android代碼和c/c++代碼,只是構(gòu)建方式和結(jié)構(gòu)不同蛾娶。

CMake相對傳統(tǒng)ndk-build的優(yōu)點(diǎn)在于:無需手動生成Java的頭文件、相對于mk文件配置更簡單潜秋、可以自動生成對應(yīng)abi*.so動態(tài)鏈接庫蛔琅、支持設(shè)置斷點(diǎn)調(diào)試(我認(rèn)為這是最方便的地方)、可以引用其他已經(jīng)生成的so庫半等。

準(zhǔn)備工作

  1. 在Android Studio 上安裝好NDK和CMake揍愁,網(wǎng)上教程很多這里就不在贅述呐萨。
  2. 下載Lame源碼。

項(xiàng)目結(jié)構(gòu)

通過這張項(xiàng)目結(jié)構(gòu)可以先幫助我們更形象整體的理解CMake構(gòu)建NDK的方式莽囤。

這里寫圖片描述

Tips:如果你對CMake剛接觸谬擦,可以先用Android Studio創(chuàng)建一個項(xiàng)目,然后勾選上include c++選項(xiàng)朽缎,去看下demo的結(jié)構(gòu)惨远,幫助理解,我就是這樣做的话肖,效果還不錯北秽。

Lame源碼移植

  1. 首先在src/main/目錄下新建一個cpp文件夾,我們可以將Lame源碼中libmp3lame拷貝到cpp文件夾下最筒,當(dāng)然這里我們也可以重命名贺氓,例如我命名為lamemp3(以下介紹我將沿用此名)。
  2. 將Lame源碼中的include文件夾下的lame.h復(fù)制到lamemp3文件夾中床蜘。
  3. 剔除lamemp3中不必要的文件和目錄辙培,只保留.c.h文件,因?yàn)槠渌募蠖喽际桥幚砦募暇猓瑢τ贏ndroid不是必需的扬蕊。
  4. 修改util.h的源碼。在570行找到ieee754_float32_t數(shù)據(jù)類型丹擎,將其修改為float類型尾抑,因?yàn)?code>ieee754_float32_t是Linux或者是Unix下支持的數(shù)據(jù)類型,在Android下并不支持蒂培。
  5. set_get.h中24行將include <lame.h>改為include "lame.h"再愈。
  6. id3tag.cmachine.h兩個文件里,將HAVE_STRCHRHAVE_MEMCPY的ifdef結(jié)構(gòu)體注釋掉毁渗,不然編譯會報(bào)錯践磅。
#ifdef STDC_HEADERS
# include <stdlib.h>
# include <string.h>
#else
/*
# ifndef HAVE_STRCHR
#  define strchr index
#  define strrchr rindex
# endif
*/
char   *strchr(), *strrchr();
/*
# ifndef HAVE_MEMCPY
#  define memcpy(d, s, n) bcopy ((s), (d), (n))
#  define memmove(d, s, n) bcopy ((s), (d), (n))
# endif
*/
#endif

CMakeLists編寫

src中新建一個名為CMakeLists.txt的文件(注意,這里的CMakeLists.txt不一定非要放到這里灸异,只要它的位置和build.gradle文件的配置相對應(yīng)就行)府适。

我們看下CMakeLists.txt的內(nèi)容,這里我把注釋已經(jīng)寫得很詳細(xì)了肺樟,大家看下就很明白了:

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.4.1)

# 定義常量
set(SRC_DIR main/cpp/lamemp3)

# 指定關(guān)聯(lián)的頭文件目錄
include_directories(main/cpp/lamemp3)

# 查找在某個路徑下的所有源文件
aux_source_directory(main/cpp/lamemp3 SRC_LIST)

# 設(shè)置 *.so 文件輸出路徑檐春,要放在在add_library之前,不然不會起作用
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

# 聲明庫名稱么伯、類型疟暖、源碼文件
add_library(lame-mp3-utils SHARED main/cpp/lame-mp3-utils.cpp ${SRC_LIST})

# 定位某個NDK庫,這里定位的是log庫
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# 將NDK庫鏈接到native庫中,這樣native庫才能調(diào)用NDK庫中的函數(shù)
target_link_libraries( # Specifies the target library.
                       lame-mp3-utils

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

build.gradle配置

android {
    ......
    defaultConfig {
        ......
        externalNativeBuild {
            cmake {
                cppFlags ""
                abiFilters 'armeabi-v7a','arm64-v8a','mips','mips64','x86','x86_64' //要支持的abi
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "src/CMakeLists.txt"http://配置文件路徑
        }
    }
}

編寫Java native方法

這里我在代碼中注釋已經(jīng)寫得非常詳細(xì)了俐巴,關(guān)于一些參數(shù)我會在下面做更詳細(xì)的解釋骨望。

public class Mp3Converter {

    static  {
        System.loadLibrary("lame-mp3-utils");
    }

    /**
     * init lame
     * @param inSampleRate
     *              input sample rate in Hz
     * @param channel
     *              number of channels
     * @param mode
     *              0 = CBR, 1 = VBR, 2 = ABR.  default = 0
     * @param outSampleRate
     *              output sample rate in Hz
     * @param outBitRate
     *              rate compression ratio in KHz
     * @param quality
     *              quality=0..9. 0=best (very slow). 9=worst.<br />
     *              recommended:<br />
     *              2 near-best quality, not too slow<br />
     *              5 good quality, fast<br />
     *              7 ok quality, really fast
     */
    public native static void init(int inSampleRate, int channel, int mode,
                                   int outSampleRate, int outBitRate, int quality);


    /**
     * file convert to mp3
     * it may cost a lot of time and better put it in a thread
     * @param input
     *          file path to be converted
     * @param mp3
     *          mp3 output file path
     */
    public native  static void convertMp3(String input, String mp3);


    /**
     * get converted bytes in inputBuffer
     * @return
     *          converted bytes in inputBuffer
     *          to ignore the deviation of the file size,when return to -1 represents convert complete
     */
    public native static long getConvertBytes();

    /**
     * get library lame version
     * @return
     */
    public native static String getLameVersion();

}

編寫調(diào)用C/C++的cpp

先看一個上面Java文件中native init(args...) 方法在這里是如何實(shí)現(xiàn)的:

extern "C" JNIEXPORT void JNICALL
Java_jaygoo_library_converter_Mp3Converter_init(JNIEnv *env, jclass type, jint inSampleRate,
                                               jint channel, jint mode, jint outSampleRate,
                                               jint outBitRate, jint quality) {
    lameInit(inSampleRate, channel, mode, outSampleRate, outBitRate, quality);
}
  • extern "C"因?yàn)槲覀儗懙氖莄pp是c++文件,所以當(dāng)我們調(diào)用一些c文件的方法時需要加上extern "C"欣舵,不然會提示找不到方法擎鸠。
  • Java_jaygoo_library_converter_Mp3Converter_init這里方法名是和Java文件中的native方法一一對應(yīng)的,這樣才能讓native方法找到對應(yīng)的cpp方法缘圈。格式是:Java_包名_類名_方法名劣光,這里包名的._代替,所以我們native的方法名命名盡量不要包含_糟把,但如果真的包含了绢涡,那么在cpp文件中用1代替Java native 中的_
  • JNIEXPORT void JNICALL是固定的格式遣疯,也是輔助native方法找到對應(yīng)的cpp方法雄可。
  • JNIEnv *envJNIEnv是指向JNINativeInterface結(jié)構(gòu)的指針,當(dāng)我們需要調(diào)用JNI方法時缠犀,都需要通過這個指針才能進(jìn)行調(diào)用滞项。

其實(shí)我們還可以通過Android Studio來自動生成這些方法和參數(shù),在Android Studio中點(diǎn)擊native方法名夭坪,快捷鍵alt+enter即可自動生成了。

看到這里过椎,大家基本對如何編寫cpp代碼有一定的了解室梅,接下來我來介紹下lame-mp3-utils.cpp的實(shí)現(xiàn),由于篇幅有限疚宇,就不全上代碼了亡鼠,這里介紹幾個比較關(guān)鍵的方法。


init

這里主要是對Lame進(jìn)行一些初始化敷待,主要的參數(shù)包括:

  1. inSampleRate 要轉(zhuǎn)換的音頻文件采樣率
  2. mode 音頻編碼模式间涵,包括VBR、ABR榜揖、CBR
  3. outSampleRate 轉(zhuǎn)換后音頻文件采樣率
  4. outBitRate 輸出的碼率
  5. quality 壓縮質(zhì)量(具體數(shù)值上面注釋已經(jīng)寫的很清楚了)

這里的代碼沒什么可看的勾哩,主要是調(diào)用一些lame自帶的方法設(shè)置一些配置參數(shù),最后調(diào)用lame_init_params(lame)完成初始化举哟,這里我對上面幾個參數(shù)出現(xiàn)的名詞做下解釋:

  • 采樣率每秒從連續(xù)信號中提取并組成離散信號的采樣個數(shù)思劳,單位Hz。數(shù)值越高妨猩,音質(zhì)越好潜叛,常見的如8000Hz、11025Hz、22050Hz威兜、32000Hz销斟、44100Hz等。
  • 碼率又稱比特率是指每秒傳送的比特(bit)數(shù)椒舵,單位kbps蚂踊,越高音質(zhì)越好(相同編碼格式下)。
  • CBR常數(shù)比特率編碼逮栅,碼率固定悴势,速度較快,但壓縮的文件相比其他模式較大措伐,音質(zhì)也不會有很大提高特纤,適用于流式播放方案,lame默認(rèn)的方案是這種侥加。
  • VBR動態(tài)比特率編碼捧存,碼率不固定。適用于下載后在本地播放或者在讀取速度有限的設(shè)備播放担败,體積和為CBR的一半左右昔穴,但是輸出碼率不可控
  • ABR平均比特率編碼,是Lame針對CBR不佳的文件體積比和VBR生成文件大小不定的特點(diǎn)獨(dú)創(chuàng)的編碼模式提前。是一種折中方案吗货,碼率基本可控,但是好像用的不多狈网。

convertMp3(jstring jInputPath, jstring jMp3Path)

首先我們要將jstring轉(zhuǎn)換為c++中的char*后才可以使用宙搬,我們可以通過JNI提供的GetStringUTFChars方法完成轉(zhuǎn)換:

const char* cInput = env->GetStringUTFChars(jInputPath, 0);
const char* cMp3 = env->GetStringUTFChars(jMp3Path, 0);

然后我們通過fopen來打開需要操作的文件,用rb來讀取輸入文件拓哺,用wb來寫轉(zhuǎn)換后的文件勇垛。

 FILE* fInput = fopen(cInputPath,"rb");
 FILE* fMp3 = fopen(cMp3Path,"wb");

接下來我們申請兩個buffer來緩存文件數(shù)據(jù),我們邊讀邊轉(zhuǎn)換士鸥,然后再將轉(zhuǎn)換后的數(shù)據(jù)寫入文件闲孤。由于Lame的要求,這里的buffer數(shù)據(jù)必須要不小于7200烤礁,下面是具體的轉(zhuǎn)換代碼:

 //convert to mp3
    do{
        //這里將輸入文件內(nèi)容讀取到inputBuffer中讼积,當(dāng)全部讀取會返回0
        read = static_cast<int>(fread(inputBuffer, sizeof(short int) * 2, 8192, fInput));
        //這里用于計(jì)算讀取的原文件的byte數(shù),可以用于計(jì)算轉(zhuǎn)換的進(jìn)度
        total +=  read * sizeof(short int)*2;
        nowConvertBytes = total;
        if(read != 0){
            //這里用lame將inputBuffer轉(zhuǎn)換為MP3格式的數(shù)據(jù)放入mp3Buffer中
            write = lame_encode_buffer_interleaved(lame, inputBuffer, read, mp3Buffer, BUFFER_SIZE);
            //將轉(zhuǎn)換好的mp3Buffer的數(shù)據(jù)寫入文件
            fwrite(mp3Buffer, sizeof(unsigned char), static_cast<size_t>(write), fMp3);
        }
        //最后全部讀取完成后及時flush
        if(read == 0){
            lame_encode_flush(lame,mp3Buffer, BUFFER_SIZE);
        }
    }while(read != 0);

最后記得轉(zhuǎn)換后釋放資源:

    resetLame();
    fclose(fInput);
    fclose(fMp3);
    env->ReleaseStringUTFChars(jInputPath, cInput);
    env->ReleaseStringUTFChars(jMp3Path, cMp3);

生成不同ABI下的so庫

為了支持不同的設(shè)備脚仔,我們需要根據(jù)不同的ABI生成不同的so庫來調(diào)用币砂,我們可以通過Android Studio的Make來調(diào)用CMakeList.txt腳本生成支持各種ABI版本的so庫。文件輸出路徑可以通過配置CMakeList.txt來修改:

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

其中PROJECT_SOURCE_DIR是指腳本所在目錄玻侥,ANDROID_ABI是指在build.gradle中配置的abiFilters决摧。

ABI擴(kuò)展知識

ABI(Application binary interface)應(yīng)用程序二進(jìn)制接口。不同的CPU 與指令集的每種組合都有定義的 ABI (應(yīng)用程序二進(jìn)制接口),一段程序只有遵循這個接口規(guī)范才能在該 CPU 上運(yùn)行掌桩,所以同樣的程序代碼為了兼容多個不同的CPU边锁,需要為不同的 ABI 構(gòu)建不同的庫文件。當(dāng)然對于CPU來說波岛,不同的架構(gòu)并不意味著一定互不兼容茅坛。

  • armeabi設(shè)備只兼容armeabi
  • armeabi-v7a設(shè)備兼容armeabi-v7a、armeabi
  • arm64-v8a設(shè)備兼容arm64-v8a则拷、armeabi-v7a贡蓖、armeabi
  • x86設(shè)備兼容X86、armeabi
  • mips64設(shè)備兼容mips64煌茬、mips
  • mips只兼容mips斥铺;

GitHub

https://github.com/Jay-Goo/Mp3Converter

參考文獻(xiàn)

https://blog.csdn.net/allen315410/article/details/42456661
http://www.reibang.com/p/6332418b12b1

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坛善,隨后出現(xiàn)的幾起案子晾蜘,更是在濱河造成了極大的恐慌,老刑警劉巖眠屎,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剔交,死亡現(xiàn)場離奇詭異,居然都是意外死亡改衩,警方通過查閱死者的電腦和手機(jī)岖常,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來葫督,“玉大人腥椒,你說我怎么就攤上這事『蜓埽” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵洒放,是天一觀的道長蛉鹿。 經(jīng)常有香客問我,道長往湿,這世上最難降的妖魔是什么妖异? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮领追,結(jié)果婚禮上他膳,老公的妹妹穿的比我還像新娘。我一直安慰自己绒窑,他們只是感情好棕孙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般蟀俊。 火紅的嫁衣襯著肌膚如雪钦铺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天肢预,我揣著相機(jī)與錄音矛洞,去河邊找鬼。 笑死烫映,一個胖子當(dāng)著我的面吹牛沼本,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锭沟,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼抽兆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冈钦?” 一聲冷哼從身側(cè)響起郊丛,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瞧筛,沒想到半個月后厉熟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡较幌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年揍瑟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乍炉。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡绢片,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岛琼,到底是詐尸還是另有隱情底循,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布槐瑞,位于F島的核電站熙涤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏困檩。R本人自食惡果不足惜祠挫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悼沿。 院中可真熱鬧等舔,春花似錦、人聲如沸糟趾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至涤浇,卻和暖如春鳖藕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背只锭。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工著恩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜻展。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓喉誊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纵顾。 傳聞我的和親對象是個殘疾皇子伍茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

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