手把手教你Android如何使用NDK實現一個MP3轉碼庫

通過本文你可以學到以下知識:

  • 如何實現一個Android MP3轉碼庫
  • 一些和音頻轉碼相關的基礎知識
  • 如何使用NDK將c/c++項目移植到Android端,并使用Java調用c/c++代碼
  • 如何使用CMake構建NDK項目
  • 如何生成不同CPU架構所需的動態鏈接庫

工具簡介

Lame

LAME 是最好的MP3編碼器,速度快,效果好,特別是中高碼率和VBR編碼方面。

NDK

原生開發工具包,即幫助開發原生代碼的一系列工具,包括但不限于編譯工具、一些公共庫、開發IDE等。它提供了完整的一套將 c/c++ 代碼編譯成靜態/動態庫的工具,而 Android.mkApplication.mk 你可以認為是描述編譯參數和一些配置的文件。比如指定使用c++11還是c++14編譯,會引用哪些共享庫,并描述關系等,還會指定編譯的abi。只有有了這些 NDK 中的編譯工具才能準確的編譯 c/c++ 代碼。

CMake簡介

CMake是一個跨平臺的編譯工具,它并不會直接編譯出對象,而是根據自定義的語言規則(CMakeLists.txt)生成 對應 makefile 或 project 文件,然后再調用底層的編譯。Android Studio 2.2以后開始支持CMake,所以現在我們有2種方式來編譯c/c++ 代碼。一個是 ndk-build + Android.mk + Application.mk 組合,另一個是 CMake + CMakeLists.txt 組合,它們都不會影響我們的android代碼和c/c++代碼,只是構建方式和結構不同。

CMake相對傳統ndk-build的優點在于:無需手動生成Java的頭文件、相對于mk文件配置更簡單、可以自動生成對應abi*.so動態鏈接庫、支持設置斷點調試(我認為這是最方便的地方)、可以引用其他已經生成的so庫。

準備工作

  1. 在Android Studio 上安裝好NDK和CMake,網上教程很多這里就不在贅述。
  2. 下載Lame源碼。

項目結構

通過這張項目結構可以先幫助我們更形象整體的理解CMake構建NDK的方式。

這里寫圖片描述

Tips:如果你對CMake剛接觸,可以先用Android Studio創建一個項目,然后勾選上include c++選項,去看下demo的結構,幫助理解,我就是這樣做的,效果還不錯。

Lame源碼移植

  1. 首先在src/main/目錄下新建一個cpp文件夾,我們可以將Lame源碼中libmp3lame拷貝到cpp文件夾下,當然這里我們也可以重命名,例如我命名為lamemp3(以下介紹我將沿用此名)。
  2. 將Lame源碼中的include文件夾下的lame.h復制到lamemp3文件夾中。
  3. 剔除lamemp3中不必要的文件和目錄,只保留.c.h文件,因為其他文件大多都是批處理文件,對于Android不是必需的。
  4. 修改util.h的源碼。在570行找到ieee754_float32_t數據類型,將其修改為float類型,因為ieee754_float32_t是Linux或者是Unix下支持的數據類型,在Android下并不支持。
  5. set_get.h中24行將include <lame.h>改為include "lame.h"
  6. id3tag.cmachine.h兩個文件里,將HAVE_STRCHRHAVE_MEMCPY的ifdef結構體注釋掉,不然編譯會報錯。
#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文件的配置相對應就行)。

我們看下CMakeLists.txt的內容,這里我把注釋已經寫得很詳細了,大家看下就很明白了:

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

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

# 指定關聯的頭文件目錄
include_directories(main/cpp/lamemp3)

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

# 設置 *.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庫才能調用NDK庫中的函數
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方法

這里我在代碼中注釋已經寫得非常詳細了,關于一些參數我會在下面做更詳細的解釋。

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();

}

編寫調用C/C++的cpp

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

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"因為我們寫的是cpp是c++文件,所以當我們調用一些c文件的方法時需要加上extern "C",不然會提示找不到方法。
  • Java_jaygoo_library_converter_Mp3Converter_init這里方法名是和Java文件中的native方法一一對應的,這樣才能讓native方法找到對應的cpp方法。格式是:Java_包名_類名_方法名,這里包名的._代替,所以我們native的方法名命名盡量不要包含_,但如果真的包含了,那么在cpp文件中用1代替Java native 中的_
  • JNIEXPORT void JNICALL是固定的格式,也是輔助native方法找到對應的cpp方法。
  • JNIEnv *envJNIEnv是指向JNINativeInterface結構的指針,當我們需要調用JNI方法時,都需要通過這個指針才能進行調用。

其實我們還可以通過Android Studio來自動生成這些方法和參數,在Android Studio中點擊native方法名,快捷鍵alt+enter即可自動生成了。

看到這里,大家基本對如何編寫cpp代碼有一定的了解,接下來我來介紹下lame-mp3-utils.cpp的實現,由于篇幅有限,就不全上代碼了,這里介紹幾個比較關鍵的方法。


init

這里主要是對Lame進行一些初始化,主要的參數包括:

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

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

  • 采樣率每秒從連續信號中提取并組成離散信號的采樣個數,單位Hz。數值越高,音質越好,常見的如8000Hz、11025Hz、22050Hz、32000Hz、44100Hz等。
  • 碼率又稱比特率是指每秒傳送的比特(bit)數,單位kbps,越高音質越好(相同編碼格式下)。
  • CBR常數比特率編碼,碼率固定,速度較快,但壓縮的文件相比其他模式較大,音質也不會有很大提高,適用于流式播放方案,lame默認的方案是這種。
  • VBR動態比特率編碼,碼率不固定。適用于下載后在本地播放或者在讀取速度有限的設備播放,體積和為CBR的一半左右,但是輸出碼率不可控
  • ABR平均比特率編碼,是Lame針對CBR不佳的文件體積比和VBR生成文件大小不定的特點獨創的編碼模式。是一種折中方案,碼率基本可控,但是好像用的不多。

convertMp3(jstring jInputPath, jstring jMp3Path)

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

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

然后我們通過fopen來打開需要操作的文件,用rb來讀取輸入文件,用wb來寫轉換后的文件。

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

接下來我們申請兩個buffer來緩存文件數據,我們邊讀邊轉換,然后再將轉換后的數據寫入文件。由于Lame的要求,這里的buffer數據必須要不小于7200,下面是具體的轉換代碼:

 //convert to mp3
    do{
        //這里將輸入文件內容讀取到inputBuffer中,當全部讀取會返回0
        read = static_cast<int>(fread(inputBuffer, sizeof(short int) * 2, 8192, fInput));
        //這里用于計算讀取的原文件的byte數,可以用于計算轉換的進度
        total +=  read * sizeof(short int)*2;
        nowConvertBytes = total;
        if(read != 0){
            //這里用lame將inputBuffer轉換為MP3格式的數據放入mp3Buffer中
            write = lame_encode_buffer_interleaved(lame, inputBuffer, read, mp3Buffer, BUFFER_SIZE);
            //將轉換好的mp3Buffer的數據寫入文件
            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);

最后記得轉換后釋放資源:

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

生成不同ABI下的so庫

為了支持不同的設備,我們需要根據不同的ABI生成不同的so庫來調用,我們可以通過Android Studio的Make來調用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擴展知識

ABI(Application binary interface)應用程序二進制接口。不同的CPU 與指令集的每種組合都有定義的 ABI (應用程序二進制接口),一段程序只有遵循這個接口規范才能在該 CPU 上運行,所以同樣的程序代碼為了兼容多個不同的CPU,需要為不同的 ABI 構建不同的庫文件。當然對于CPU來說,不同的架構并不意味著一定互不兼容。

  • armeabi設備只兼容armeabi
  • armeabi-v7a設備兼容armeabi-v7a、armeabi
  • arm64-v8a設備兼容arm64-v8a、armeabi-v7a、armeabi
  • x86設備兼容X86、armeabi
  • mips64設備兼容mips64、mips
  • mips只兼容mips;

GitHub

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

參考文獻

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容