通過本文你可以學(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.mk
和 Application.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)備工作
- 在Android Studio 上安裝好NDK和CMake揍愁,網(wǎng)上教程很多這里就不在贅述呐萨。
- 下載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源碼移植
- 首先在
src/main/
目錄下新建一個cpp
文件夾,我們可以將Lame源碼中libmp3lame
拷貝到cpp
文件夾下最筒,當(dāng)然這里我們也可以重命名贺氓,例如我命名為lamemp3
(以下介紹我將沿用此名)。 - 將Lame源碼中的
include
文件夾下的lame.h
復(fù)制到lamemp3
文件夾中床蜘。 - 剔除
lamemp3
中不必要的文件和目錄辙培,只保留.c
和.h
文件,因?yàn)槠渌募蠖喽际桥幚砦募暇猓瑢τ贏ndroid不是必需的扬蕊。 - 修改
util.h
的源碼。在570行找到ieee754_float32_t
數(shù)據(jù)類型丹擎,將其修改為float
類型尾抑,因?yàn)?code>ieee754_float32_t是Linux或者是Unix下支持的數(shù)據(jù)類型,在Android下并不支持蒂培。 -
set_get.h
中24行將include <lame.h>
改為include "lame.h"
再愈。 - 在
id3tag.c
和machine.h
兩個文件里,將HAVE_STRCHR
和HAVE_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 *env
JNIEnv是指向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ù)包括:
- inSampleRate 要轉(zhuǎn)換的音頻文件采樣率
- mode 音頻編碼模式间涵,包括VBR、ABR榜揖、CBR
- outSampleRate 轉(zhuǎn)換后音頻文件采樣率
- outBitRate 輸出的碼率
- 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