什麼是Lame
- Lame是一個C語言MP3編碼庫
- Lame源碼本身是運行在PC平臺的,我們對其進行了稍加修改,使其適用於Android平臺
- PCM轉MP3是安卓開發中常見的需求,PCM是未經處理的原生音頻數據,安卓錄音得到的都是PCM數據,但是一般網站都會要求MP3格式的,因爲PCM體積太大,所以PCM轉MP3是非常常見的需求
- C++一般負責的都是音視頻開發,硬件通信,進程控制等工作,而Lame庫又是音視頻開發當中最常見最簡單的功能,很適合作爲我們學習NDK的入門材料
下載Android Lame源碼和庫
這裏先給出源碼,對於剛學習NDK的新手來說,很多細節都可能出問題,有一個問題就編譯不過,所以先給出代碼
除了Lame源碼部分,建議新手親自編寫全部代碼,並弄清全部原理。其實NDK開發就那麼多東西,所有坑都踩過,知道解決方法和其中原理,NDK就學得差不多了。早點踩坑,就離熟練掌握更近一步
這裏給出兩個鏈接,一個是Demo源碼,適用於那些抱着學習態度的人,另一個是編譯好的so庫和JNA接口,適用於那些急着拿成品直接開發應用的人
AndroidLame完整源碼:Project-Lame
編譯好的so庫和JNA接口:Liblame
核心代碼
這裏給出除了Lame源碼外的所有代碼和編譯配置
開啓NDK編譯功能,添加JNA依賴,添加so庫依賴
注意,安卓上的JNA的需要依賴一個libjnidispatch.so文件,這和在PC上通過JNA加載DLL是不一樣的
//build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.easing.android"
minSdkVersion 23
targetSdkVersion 29
javaCompileOptions {
annotationProcessorOptions {
includeCompileClasspath = true
}
}
//過濾CPU架構,只使用armv7的庫
ndk {
abiFilters "armeabi-v7a"
}
}
//加載指定位置的so庫
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
//編譯jni目錄,開啓這個選項後,會自動編譯C++代碼生成so文件,並自動引用
//開啓此選項後,就不需要再將so庫拷貝到sourceSets指定的位置了,否則會引起so庫重複錯誤
externalNativeBuild {
ndkBuild {
path 'src/jni/Android.mk'
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
dependencies {
api project(':android-commons')
api 'net.java.dev.jna:jna:5.5.0'
}
NDK編譯配置
//Android.mk
#記錄當前路徑
LOCAL_PATH := $(call my-dir)
#清除默認的LOCAL變量
include $(CLEAR_VARS)
#模塊名稱
LOCAL_MODULE := liblame
#鏈接系統模塊
LOCAL_LDLIBS := -lm -llog
#源碼列表
#建議通過工具自動遍歷所有源碼文件,生成路徑列表
#不建議通過指令遍歷所有源碼,較爲複雜且難以閱讀
LOCAL_SRC_FILES := JnaLame.cpp logcat.cpp liblame/bitstream.c liblame/bitstream.h liblame/encoder.c liblame/encoder.h liblame/fft.c liblame/fft.h liblame/gain_analysis.c liblame/gain_analysis.h liblame/id3tag.c liblame/id3tag.h liblame/l3side.h liblame/lame-analysis.h liblame/lame.c liblame/lame.h liblame/lameerror.h liblame/lame_global_flags.h liblame/machine.h liblame/mpglib_interface.c liblame/newmdct.c liblame/newmdct.h liblame/presets.c liblame/psymodel.c liblame/psymodel.h liblame/quantize.c liblame/quantize.h liblame/quantize_pvt.c liblame/quantize_pvt.h liblame/reservoir.c liblame/reservoir.h liblame/set_get.c liblame/set_get.h liblame/tables.c liblame/tables.h liblame/takehiro.c liblame/util.c liblame/util.h liblame/vbrquantize.c liblame/vbrquantize.h liblame/VbrTag.c liblame/VbrTag.h liblame/version.c liblame/version.h
#引入本地庫
#此處引入C++標準庫作爲測試
#由於Application.mk裏面已經開啓了標準庫,這裏其實是多餘的
LOCAL_C_INCLUDES := D:/dev/sdk/ndk/21.0.6113669/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a
LOCAL_C_INCLUDES += D:/dev/sdk/ndk/21.0.6113669/sources/cxx-stl/llvm-libc++/libs/arm64-v8a/libc++_static.a
#指示編譯器生成動態庫
#如果不生成動態庫,則不需要Application.mk文件
include $(BUILD_SHARED_LIBRARY)
so庫編譯配置
//Application.mk
#指定要適配的CPU架構
APP_ABI := armeabi-v7a
#最低安卓版本要求
APP_PLATFORM := android-23
#忽略格式安全檢查
APP_CPPFLAGS := -Wno-error=format-security
#開啓C++標準庫
APP_STL := c++_static
通過std::cout打印內容到控制檯
//logcat.cpp
#ifndef LOGCAT_CPP
#define LOGCAT_CPP
#include <jni.h>
#include <iostream>
#include <stdio.h>
#include <streambuf>
#include <unistd.h>
#include <android/log.h>
static int pfd[2];
static pthread_t tid;
static const char *TAG = "NativeLogger";
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
//子線程,不斷從FileDescription中讀取數據並打印
static void *threadFunc(void *) {
ssize_t byteCount;
char buffer[1024];
while ((byteCount = read(pfd[0], buffer, sizeof buffer - 1)) > 0) {
if (buffer[byteCount - 1] == '\n') --byteCount;
buffer[byteCount] = 0;
LOGI(buffer);
}
return 0;
}
//開啓JNI日誌服務
static int startNativeLogger(const char *tag) {
TAG = tag;
//將stdout與stderror與緩衝區解綁
setvbuf(stdout, nullptr, _IOLBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
//打開FileDescription,並將stdout和stderror重定向到FileDescription
pipe(pfd);
dup2(pfd[1], 1);
dup2(pfd[1], 2);
//開啓子線程,不斷從FileDescription中讀取數據並打印
if (pthread_create(&tid, 0, threadFunc, 0) == -1)
return -1;
pthread_detach(tid);
return 0;
}
#endif
Lame暴漏接口給JNA調用
//JnaLame.cpp
#include "logcat.cpp"
#include "liblame/lame.h"
//使用std::cout前需先調用此方法,設置打印標誌,開啓打印服務
extern "C" int initNativeLogger(const char *tag) {
startNativeLogger(tag);
std::cout << "Bind Standard Output to Logcat" << std::endl;
return 0;
}
FILE *pcmFile;
FILE *mp3File;
LAME *lame;
bool singlechannel;
//初始化
extern "C" int init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate) {
int ret = -1;
if (channels < 1) channels = 1;
if (channels > 2) channels = 2;
singlechannel = channels == 1;
pcmFile = fopen(pcmFilePath, "rb");
if (pcmFile) {
mp3File = fopen(mp3FilePath, "wb");
if (mp3File) {
lame = lame_init();
lame_set_in_samplerate(lame, sampleRate);
lame_set_out_samplerate(lame, sampleRate);
lame_set_num_channels(lame, channels);
lame_set_brate(lame, bitRate);
lame_init_params(lame);
ret = 0;
}
}
if (ret == 0)
std::cout << "Lame Init Success" << std::endl;
else
std::cout << "Lame Init Fail" << std::endl;
return ret;
}
//轉碼單聲道PCM
void encodeSingleChannelPcm() {
int bufferSize = 1024 * 256;
short *buffer = new short[bufferSize / 2];
unsigned char *mp3Buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
while ((readBufferSize = fread(buffer, 2, bufferSize / 2, pcmFile)) > 0) {
size_t wroteSize = lame_encode_buffer(lame, buffer, nullptr, readBufferSize, mp3Buffer, bufferSize);
fwrite(mp3Buffer, 1, wroteSize, mp3File);
}
delete[] buffer;
delete[] mp3Buffer;
}
//轉碼雙聲道PCM
void encodeDoubleChannelPcm() {
int bufferSize = 1024 * 256;
short *buffer = new short[bufferSize / 2];
short *leftBuffer = new short[bufferSize / 4];
short *rightBuffer = new short[bufferSize / 4];
unsigned char *mp3Buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
while ((readBufferSize = fread(buffer, 2, bufferSize / 2, pcmFile)) > 0) {
for (int i = 0; i < readBufferSize; i++) {
if (i % 2 == 0)
leftBuffer[i / 2] = buffer[i];
else
rightBuffer[i / 2] = buffer[i];
}
size_t wroteSize = lame_encode_buffer(lame, leftBuffer, rightBuffer, readBufferSize / 2, mp3Buffer, bufferSize);
fwrite(mp3Buffer, 1, wroteSize, mp3File);
}
delete[] buffer;
delete[] leftBuffer;
delete[] rightBuffer;
delete[] mp3Buffer;
}
//轉碼
extern "C" void encode() {
if (singlechannel)
encodeSingleChannelPcm();
else
encodeDoubleChannelPcm();
}
//釋放
extern "C" void dispose() {
if (pcmFile)
fclose(pcmFile);
if (mp3File) {
fclose(mp3File);
lame_close(lame);
}
}
JNA調用Native接口
//JnaLame.java
import com.sun.jna.Library;
import com.sun.jna.Native;
public interface JnaLame extends Library {
JnaLame INSTANCE = Native.loadLibrary("lame", JnaLame.class);
int initNativeLogger(String tag);
int init(String pcmFilePath, String mp3FilePath, int sampleRate, int channels, int bitRate);
void encode();
void dispose();
}
通過JNA使用Lame
//Activity.java
//寫一個PCM文件到存儲卡
byte[] pcmBytes = Resources.readAssetBytes("abc.pcm");
Files.writeToFile("sdcard/x.pcm", pcmBytes);
//創建MP3輸出文件
Files.createFile("sdcard/x.mp3");
//初始化C++日誌功能
JnaLame.INSTANCE.initNativeLogger("JnaLame");
//初始化Lame
JnaLame.INSTANCE.init("sdcard/x.pcm", "sdcard/x.mp3", 44100, 1, 16);
//通過Lame將PCM轉MP3
JnaLame.INSTANCE.encode();
//釋放Lame
JnaLame.INSTANCE.dispose();
學習價值
這雖然是一個比較簡單的NDK案例,但是新手如果從頭自己寫,還是可能會遇到很多問題,從中學到不少東西的
- 配置Gradle自動編譯Native代碼
- 在Android中使用JNA庫
- C/C++語言的熟練使用
- 在Native代碼中暴漏接口給JNA
- 編寫,加載和調用JNA接口
- 在Native代碼中使用std標準庫
- 將std輸出流重定向到Logcat控制檯
- Android.mk和Application.mk編譯腳本配置
- Lame庫的使用
大家如果能自己手動熟練完成這個練習,可以說NDK開發的基本流程已經掌握得比較紮實了