【NDK】【008】NDK編譯Lame庫

什麼是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開發的基本流程已經掌握得比較紮實了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章