天天看點

Android音視訊開發:音頻非壓縮編碼和壓縮編碼

音視訊在開發中,最重要也是最複雜的就是編解碼的過程,在上一篇的《Android音視訊開發:踩一踩“門檻”》中,我們說音頻的編碼根據大小劃分有兩種:壓縮編碼和非壓縮編碼,那到底是怎麼實作的這兩中編碼的呢?這一次就詳細了解Android中如何使用這兩種方式進行音頻編碼

前景提要

這裡先回顧一下音頻的壓縮編碼和非壓縮編碼:

  • 非壓縮編碼:音頻裸資料,也即是我們所說的PCM
  • 壓縮編碼:對資料進行壓縮,壓縮不能被人耳感覺到的備援信号

因為非壓縮編碼實在是太大了,是以我們生活中所接觸的音頻編碼格式都是壓縮編碼,而且是有損壓縮,比如 MP3或AAC。

那如何操作PCM資料呢?Android SDK中提供了一套對PCM操作的API:

AudioRecord

AudioTrack

由于

AudioRecord(錄音)

AudioTrack(播放)

操作過于底層而且過于複雜,是以Android SDK 還提供了一套與之對應更加進階的API:

MediaRecorder(錄音)

MediaPlayer(播放)

,用于音視訊的操作,當然其更加簡單友善。我們這裡隻介紹前者,通過它來實作對PCM資料的操作。

對于壓縮編碼,我們則通過

MediaCodec

Lame

來分别實作AAC音頻和Mp3音頻壓縮編碼。話不多說,請往下看!

AudioRecord

AudioRecord

更加底層,能夠更好的并且直接的管理通過音頻錄制硬體裝置錄制後的PCM資料,是以對資料處理更加靈活,但是同時也需要我們自己處理編碼的過程。

AudioRecord的使用流程大緻如下:

  • 根據音頻參數建立

    AudioRecord

  • 調用

    startRecording

    開始錄制
  • 開啟錄制線程,通過

    AudioRecord

    将錄制的音頻資料從緩存中讀取并寫入檔案
  • 釋放資源

在使用

AudioRecord

前需要先注意添加

RECORD_AUDIO

錄音權限。

建立AudioRecord

我們先看看

AudioRecord

構造方法

public AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)複制代碼      
  • audioSource,從字面意思可知音頻來源,由

    MediaRecorder.AudioSource

    提供,主要有以下内容

    ·  CAMCORDER 與照相機方向相同的麥克風音頻源

    ·  DEFAULT  預設

    ·  MIC 麥克風音頻源

    ·  VOICE_CALL 語音通話

    這裡采用

    MIC

    麥克風音頻源
  • sampleRateInHz,采樣率,即錄制的音頻每秒鐘會有多少次采樣,可選用的采樣頻率清單為:8000、16000、22050、24000、32000、44100、48000等,一般采用人能聽到最大音頻的2倍,也就是44100Hz。
  • channelConfig,聲道數的配置,可選值以常量的形式配置在類AudioFormat中,常用的是CHANNEL_IN_MONO(單聲道)、CHANNEL_IN_STEREO(雙聲道)
  • audioFormat,采樣格式,可選值以常量的形式定義在類AudioFormat中,分别為ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),一般采用16bit。
  • bufferSizeInBytes,其配置的是AudioRecord内部的音頻緩沖區的大小,可能會因為生産廠家的不同而有所不同,為了友善AudioRecord提供了一個擷取該值最小緩沖區大小的方法

    getMinBufferSize

public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)複制代碼      

在開發過程中需使用

getMinBufferSize

此方法計算出最小緩存大小。

切換錄制狀态

首先通過調用

getState

判斷AudioRecord是否初始化成功,然後通過

startRecording

切換成錄制狀态

if (null!=audioRecord && audioRecord?.state!=AudioRecord.STATE_UNINITIALIZED){
    audioRecord?.startRecording()
}複制代碼      

開啟錄制線程

thread = Thread(Runnable {
   writeData2File()
})
thread?.start()複制代碼      

開啟錄音線程将錄音資料通過AudioRecord寫入檔案

private fun writeData2File() {
    var ret = 0
    val byteArray = ByteArray(bufferSizeInBytes)
    val file = File(externalCacheDir?.absolutePath + File.separator + filename)if (file.exists()) {
        file.delete()
    } else {
        file.createNewFile()
    }
    val fos = FileOutputStream(file)while (status == Status.STARTING) {
        ret = audioRecord?.read(byteArray, 0, bufferSizeInBytes)!!if (ret!=AudioRecord.ERROR_BAD_VALUE || ret!=AudioRecord.ERROR_INVALID_OPERATION|| ret!=AudioRecord.ERROR_DEAD_OBJECT){
            fos.write(byteArray)
        }
    }
    fos.close()
}複制代碼      

首先停止錄制

if (null!=audioRecord && audioRecord?.state!=AudioRecord.STATE_UNINITIALIZED){
    audioRecord?.stop()
}複制代碼      

然後停止線程

if (thread!=null){
    thread?.join()
    thread =null
}複制代碼      

最後釋放AudioRecord

if (audioRecord != null) {
    audioRecord?.release()
    audioRecord = null
}複制代碼      

通過以上一個流程之後,就可以得到一個非壓縮編碼的PCM資料了。

但是這個資料在音樂播放器上一般是播放不了的,那麼怎麼驗證我是否錄制成功呢?當然是使用我們的

AudioTrack

進行播放看看是不是剛剛我們錄制的聲音了。

【完整代碼-AudioRecord】

AudioTrack

AudioTrack

是由Android SDK提供比較底層的播放API,也隻能操作PCM裸資料,通過直接渲染PCM資料進行播放。當然如果想要使用

AudioTrack

進行播放,那就需要自行先将壓縮編碼格式檔案解碼。

AudioTrack的使用流程大緻如下:

  • AudioTrack

  • play

    開始播放
  • 開啟播放線程,循環想

    AudioTrack

    緩存區寫入音頻資料

建立AudioTrack

我們來看看

AudioTrack

的構造方法

public AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode, int sessionId)複制代碼      
  • streamType,Android手機上提供音頻管理政策,按下音量鍵我們會發現由媒體聲音管理,鬧鈴聲音管理,通話聲音管理等等,當系統有多個程序需要播放音頻的時候,管理政策會決定最終的呈現效果,該參數的可選值将以常量的形式定義在類AudioManager中,主要包括以下内容:

    ·  STREAM_VOCIE_CALL:電話聲音

    ·  STREAM_SYSTEM:系統聲音

    ·  STREAM_RING:鈴聲

    ·  STREAM_MUSCI:音樂聲

    ·  STREAM_ALARM:警告聲

    ·  STREAM_NOTIFICATION:通知聲

因為這裡是播放音頻,是以我們選擇

STREAM_MUSCI

  • sampleRateInHz,采樣率,即播放的音頻每秒鐘會有多少次采樣,可選用的采樣頻率清單為:8000、16000、22050、24000、32000、44100、48000等,一般采用人能聽到最大音頻的2倍,也就是44100Hz。
  • channelConfig,聲道數的配置,可選值以常量的形式配置在類AudioFormat中,常用的是CHANNEL_IN_MONO(單聲道)、CHANNEL_IN_STEREO(立體雙聲道)
  • bufferSizeInBytes,其配置的是AudioTrack内部的音頻緩沖區的大小,可能會因為生産廠家的不同而有所不同,為了友善AudioTrack提供了一個擷取該值最小緩沖區大小的方法

    getMinBufferSize

  • mode,播放模式,AudioTrack提供了兩種播放模式,可選的值以常量的形式定義在類AudioTrack中,一個是MODE_STATIC,需要一次性将所有的資料都寫入播放緩沖區中,簡單高效,通常用于播放鈴聲、系統提醒的音頻片段;另一個是MODE_STREAM,需要按照一定的時間間隔不間斷地寫入音頻資料,理論上它可以應用于任何音頻播放的場景。
  • sessionId,AudioTrack都需要關聯一個會話Id,在建立AudioTrack時可直接使用

    AudioManager.AUDIO_SESSION_ID_GENERATE

    ,或者在構造之前通過

    AudioManager.generateAudioSessionId

    擷取。

上面這種構造方法已經被棄用了,現在基本使用如下構造(最小skd 版本需要>=21),參數内容與上基本一緻:

public AudioTrack (AudioAttributes attributes, 
                AudioFormat format, int bufferSizeInBytes, int mode, int sessionId)複制代碼      

通過

AudioAttributes.Builder

設定參數streamType

var audioAttributes = AudioAttributes.Builder()
    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
    .build()複制代碼      

AudioFormat.Builder

設定channelConfig,sampleRateInHz,audioFormat參數

var mAudioFormat = AudioFormat.Builder()
    .setChannelMask(channel)
    .setEncoding(audioFormat)
    .setSampleRate(sampleRate)
    .build()複制代碼      

切換播放狀态

getState

play

切換成錄播放狀态

if (null!=audioTrack && audioTrack?.state != AudioTrack.STATE_UNINITIALIZED){
    audioTrack?.play()
}複制代碼      

開啟播放線程

thread= Thread(Runnable {
    readDataFromFile()
})
thread?.start()複制代碼      

将資料不斷的送入緩存區并通過AudioTrack播放

private fun readDataFromFile() {
    val byteArray = ByteArray(bufferSizeInBytes)


    val file = File(externalCacheDir?.absolutePath + File.separator + filename)if (!file.exists()) {
        Toast.makeText(this, "請先進行錄制PCM音頻", Toast.LENGTH_SHORT).show()return}
    val fis = FileInputStream(file)
    var read: Int
    status = Status.STARTINGwhile ({ read = fis.read(byteArray);read }() > 0) {
        var ret = audioTrack?.write(byteArray, 0, bufferSizeInBytes)!!if (ret == AudioTrack.ERROR_BAD_VALUE || ret == AudioTrack.ERROR_INVALID_OPERATION || ret == AudioManager.ERROR_DEAD_OBJECT) {break}
    }
    fis.close()
}複制代碼      

首先停止播放

if (audioTrack != null && audioTrack?.state != AudioTrack.STATE_UNINITIALIZED) {
    audioTrack?.stop()
}複制代碼      
if (thread!=null){
    thread?.join()
    thread =null
}複制代碼      

最後釋放AudioTrack

if (audioTrack != null) {
    audioTrack?.release()
    audioTrack = null
}複制代碼      

經過這樣幾個步驟,我們就可以聽到剛剛我們錄制的PCM資料聲音啦!這就是使用Android提供的

AudioRecord

AudioTrack

對PCM資料進行操作。

但是僅僅這樣是不夠的,因為我們生活中肯定不是使用PCM進行音樂播放,那麼怎麼才能讓音頻在主流播放器上播放呢?這就需要我們進行壓縮編碼了,比如mp3或aac壓縮編碼格式。

【完整代碼-AudioTrack】

MediaCodec編碼AAC

AAC

壓縮編碼是一種高壓縮比的音頻壓縮算法,AAC壓縮比通常為18:1;采樣率範圍通常是8KHz~96KHz,這個範圍比MP3更廣一些(MP3的範圍一般是:16KHz~48KHz),是以在16bit的采樣格式上比MP3更精細。

友善我們處理AAC編碼,Android SDK中提供了

MediaCodec

API,可以将PCM資料編碼成AAC資料。大概需要以下幾個步驟:

  • 建立

    MediaCodec

  • MediaCodec

    配置音頻參數
  • 啟動線程,循環往緩沖區送入資料
  • MediaCodec

    将緩沖區的資料進行編碼并寫入檔案

建立MediaCodec

MediaCodec.createEncoderByType

建立編碼MediaCodec

mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)複制代碼      

// 配置采樣率和聲道數mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE,sampleRate,channel)// 配置比特率mediaFormat?.setInteger(MediaFormat.KEY_BIT_RATE,bitRate)// 配置PROFILE,其中屬AAC-LC相容性最好mediaFormat?.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)// 最大輸入大小mediaFormat?.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 10 * 1024)
    
mediaCodec!!.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE)
mediaCodec?.start()

inputBuffers = mediaCodec?.inputBuffers
outputBuffers = mediaCodec?.outputBuffers複制代碼      

啟動線程

啟動線程,循環讀取PCM資料送入緩沖區

thread = Thread(Runnable {val fis = FileInputStream(pcmFile)
    fos = FileOutputStream(aacFile)var read: Intwhile ({ read = fis.read(byteArray);read }() > 0) {
        encode(byteArray)
    }
})
thread?.start()複制代碼      

AAC編碼

将送入的PCM資料通過

MediaCodec

進行編碼,大緻流程如下:

  • 通過可用緩存去索引,擷取可用輸入緩沖區
  • 将pcm資料放入輸入緩沖區并送出
  • 根據輸出緩沖區索引,擷取輸出緩沖區
  • 建立輸出資料

    data

    ,并添加ADTS頭部資訊(有7byte)
  • outputBuffer

    編碼後資料寫入

    data

    (data有7byte偏移)
  • 将編碼資料

    data

    寫入檔案
  • 重複以上過程
private fun encode(byteArray: ByteArray){
    mediaCodec?.run {
        //傳回要用有效資料填充的輸入緩沖區的索引, -1 無限期地等待輸入緩沖區的可用性
        val inputIndex = dequeueInputBuffer(-1)if (inputIndex > 0){
            // 根據索引擷取可用輸入緩存區
            val inputBuffer = [email protected]!![inputIndex]
            // 清空緩沖區
            inputBuffer.clear()
            // 将pcm資料放入緩沖區
            inputBuffer.put(byteArray)
            // 送出放入資料緩沖區索引以及大小
            queueInputBuffer(inputIndex,0,byteArray.size,System.nanoTime(),0)
        }
        // 指定編碼器緩沖區中有效資料範圍
        val bufferInfo = MediaCodec.BufferInfo()
        // 擷取輸出緩沖區索引
        var outputIndex = dequeueOutputBuffer(bufferInfo,0)        while (outputIndex>0){
            // 根據索引擷取可用輸出緩存區
            val outputBuffer [email protected]!![outputIndex]
            // 測量輸出緩沖區大小
            val bufferSize = bufferInfo.size
            // 輸出緩沖區實際大小,ADTS頭部長度為7
            val bufferOutSize = bufferSize+7
            
            // 指定輸出緩存區偏移位置以及限制大小
            outputBuffer.position(bufferInfo.offset)
            outputBuffer.limit(bufferInfo.offset+bufferSize)
            // 建立輸出空資料
            val data = ByteArray(bufferOutSize)
            // 向空資料先增加ADTS頭部
            addADTStoPacket(data, bufferOutSize)
            // 将編碼輸出資料寫入已加入ADTS頭部的資料中
            outputBuffer.get(data,7,bufferInfo.size)
            // 重新指定輸出緩存區偏移
            outputBuffer.position(bufferInfo.offset)
            // 将擷取的資料寫入檔案
            fos?.write(data)
            // 釋放輸出緩沖區
            releaseOutputBuffer(outputIndex,false)
            // 重新擷取輸出緩沖區索引
            outputIndex=dequeueOutputBuffer(bufferInfo,0)
        }
    }
}複制代碼      

編碼完成後,一定要釋放所有資源,首先關閉輸入輸出流

fos?.close()
fis.close()複制代碼      

停止編碼

if (mediaCodec!=null){
     mediaCodec?.stop()
}複制代碼      

然後就是關閉線程

if (thread!=null){
    thread?.join()
    thread =null
}複制代碼      

最後釋放MediaCodec

if (mediaCodec!=null){
    mediaCodec?.release()
    mediaCodec = null

    mediaFormat = null
    inputBuffers = null
    outputBuffers = null
}複制代碼      

通過以上一個流程,我們就可以得到一個AAC壓縮編碼的音頻檔案,可以聽一聽是不是自己剛剛錄制的。我聽了一下我自己唱的一首歌,覺得我的還是可以的嘛,也不是那麼五音不全~~

【完整代碼-MediaCodec】

Android NDK

雖然我們通過壓縮編碼生成了AAC音頻檔案,但是有個問題:畢竟AAC音頻不是主流的音頻檔案呀,我們最常見的是MP3的嘛,可不可以将PCM編碼成MP3呢?

當然是可以的,但是Android SDK沒有直接提供這樣的API,隻能使用Android NDK,通過交叉編譯其他C或C++庫來進行實作。

Android NDK 是由Google提供一個工具集,可讓您使用 C 和 C++ 等語言實作應用。

Android NDK 一般有兩個用途,一個是進一步提升裝置性能,以降低延遲,或運作計算密集型應用,如遊戲或實體模拟;另一個是重複使用您自己或其他開發者的 C 或 C++ 庫。當然我們使用最多的應該還是後者。

想使用Android NDK調試代碼需要以下工具:

  • Android 原生開發套件 (NDK):這套工具使您能在 Android 應用中使用 C 和 C++ 代碼。
  • CMake:一款外部編譯工具,可與 Gradle 搭配使用來編譯原生庫。如果您隻計劃使用 ndk-build,則不需要此元件。
  • LLDB:Android Studio 用于調試原生代碼的調試程式。

可以進入Tools > SDK Manager > SDK Tools 選擇  NDK (Side by side) 和 CMake 應用安裝

在應用以上選項之後,我們可以看到SDK的目錄中多了一個

ndk-bundle

的檔案夾,大緻目錄結構如下

  • ndk-build:該Shell腳本是Android NDK建構系統的起始點,一般在項目中僅僅執行這一個指令就可以編譯出對應的動态連結庫了,後面的編譯mp3lame 就會使用到。
  • platforms:該目錄包含支援不同Android目标版本的頭檔案和庫檔案,NDK建構系統會根據具體的配置來引用指定平台下的頭檔案和庫檔案。
  • toolchains:該目錄包含目前NDK所支援的不同平台下的交叉編譯器——ARM、x86、MIPS,其中比較常用的是ARM和x86。不論是哪個平台都會提供以下工具:

    ·CC:編譯器,對C源檔案進行編譯處理,生成彙編檔案。

    ·AS:将彙編檔案生成目标檔案(彙編檔案使用的是指令助記符,AS将它翻譯成機器碼)。

    ·AR:打包器,用于庫操作,可以通過該工具從一個庫中删除或者增加目标代碼子產品。

    ·LD:連結器,為前面生成的目标代碼配置設定位址空間,将多個目标檔案連結成一個庫或者是可執行檔案。

    ·GDB:調試工具,可以對運作過程中的程式進行代碼調試工作。

    ·STRIP:以最終生成的可執行檔案或者庫檔案作為輸入,然後消除掉其中的源碼。

    ·NM:檢視靜态庫檔案中的符号表。

    ·Objdump:檢視靜态庫或者動态庫的方法簽名。

了解Android NDK 之後,就可建立一個支援C/C++ 的Android項目了:

  • 在向導的 Choose your project 部分中,選擇 Native C++ 項目類型。
  • 點選 Next。
  • 填寫向導下一部分中的所有其他字段。
  • 在向導的 Customize C++ Support 部分中,您可以使用 C++ Standard 字段來自定義項目。使用下拉清單選擇您想要使用哪種 C++ 标準化。選擇 Toolchain Default 可使用預設的 CMake 設定。
  • 點選 Finish,同步完成之後會出現如下圖所示的目錄結構,即表示原生項目建立完成

編譯Lame

LAME是一個開源的MP3音頻壓縮庫,目前是公認有損品質MP3中壓縮效果最好的編碼器,是以我們選擇它來進行壓縮編碼,那如何進行壓縮編碼呢?主流的由兩種方式:

  • Cmake
  • ndk-build

下面就詳細講解這兩種方式

Cmake編譯Lame

配置Cmake之後可以直接将Lame代碼運作于Android中

準備

下載下傳Lame-3.100并解壓大概得到如下目錄

然後将裡面的

libmp3lame

檔案夾拷貝到我們上面建立的支援c/c++項目,删除其中的i386和vector檔案夾,以及其他非.c 和 .h 字尾的檔案

需要将以下檔案進行修改,否則會報錯

  • 将util.h中570行
extern ieee754_float32_t fast_log2(ieee754_float32_t x)複制代碼      

替換成

extern float fast_log2(float x)複制代碼      
  • 在id3tag.c和machine.h兩個檔案中,将

    HAVE_STRCHR

    HAVE_MEMCPY

    注釋
#ifdef STDC_HEADERS
# include <stddef.h>
# include <stdlib.h>
# include <string.h>
# include <ctype.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))
# endif*/
#endif複制代碼      
  • 在fft.c中,将47行注釋
//#include "vector/lame_intrin.h"複制代碼      
  • 将set_get.h中24行
#include <lame.h>複制代碼      
#include "lame.h"複制代碼      

編寫Mp3編碼器

首先在自己的包下(我這裡是

com.coder.media

,這個很重要,後面會用到),建立

Mp3Encoder

的檔案,大概如下幾個方法

  • init,将聲道,比特率,采樣率等資訊傳入
  • encode,根據init中提供的資訊進行編碼
  • destroy,釋放資源
class Mp3Encoder {companion object {
        init {
            System.loadLibrary("mp3encoder")
        }
    }

    external fun init(
        pcmPath: String,
        channel: Int,
        bitRate: Int,
        sampleRate: Int,
        mp3Path: String): Intexternal fun encode()external fun destroy()}複制代碼      

在cpp目錄下建立兩個檔案

  • mp3-encoder.h
  • mp3-encoder.cpp

這兩個檔案中可能會提示錯誤異常,先不要管它,這是因為我們還沒有配置

CMakeList.txt

導緻的。

mp3-encoder.h

中定義三個變量

FILE* pcmFile;
FILE* mp3File;lame_t lameClient;複制代碼      

然後在

mp3-encoder.c

中分别實作我們在

Mp3Encoder

中定義的三個方法

首先導入需要的檔案

#include <jni.h>#include <string>#include "android/log.h"#include "libmp3lame/lame.h"#include "mp3-encoder.h"#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , "mp3-encoder", __VA_ARGS__)複制代碼      

然後實作init方法

extern "C" JNIEXPORT jint JNICALLJava_com_coder_media_Mp3Encoder_init(JNIEnv *env, jobject obj, jstring pcmPathParam, jint channels,
                                     jint bitRate, jint sampleRate, jstring mp3PathParam) {
    LOGD("encoder init");int ret = -1;const char* pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);const char* mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);
    pcmFile = fopen(pcmPath,"rb");if (pcmFile){
        mp3File = fopen(mp3Path,"wb");if (mp3File){
            lameClient = lame_init();
            lame_set_in_samplerate(lameClient, sampleRate);
            lame_set_out_samplerate(lameClient,sampleRate);
            lame_set_num_channels(lameClient,channels);
            lame_set_brate(lameClient,bitRate);
            lame_init_params(lameClient);
            ret = 0;
        }
    }
    env->ReleaseStringUTFChars(mp3PathParam, mp3Path);
    env->ReleaseStringUTFChars(pcmPathParam, pcmPath);return ret;
}複制代碼      

這個方法的作用就是将我們的音頻參數資訊送入

lameClient

需要注意我這裡的方法

Java_com_coder_media_Mp3Encoder_init

中的

com_coder_media

需要替換成你自己的對應包名,下面的encode和destroy也是如此,切記!!!

實作通過

lame

編碼encode

extern "C" JNIEXPORT void JNICALLJava_com_coder_media_Mp3Encoder_encode(JNIEnv *env, jobject obj) {
    LOGD("encoder encode");int bufferSize = 1024 * 256;short* buffer = new short[bufferSize / 2];short* leftBuffer = new short[bufferSize / 4];short* rightBuffer = new short[bufferSize / 4];unsigned char* mp3_buffer = 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(lameClient, (short int *) leftBuffer, (short int *) rightBuffer,
                                              (int)(readBufferSize / 2), mp3_buffer, bufferSize);
        fwrite(mp3_buffer, 1, wroteSize, mp3File);
    }delete[] buffer;delete[] leftBuffer;delete[] rightBuffer;delete[] mp3_buffer;
}複制代碼      

最後釋放資源

extern "C" JNIEXPORT void JNICALL
Java_com_coder_media_Mp3Encoder_destroy(JNIEnv *env, jobject obj) {
    LOGD("encoder destroy");if(pcmFile) {
        fclose(pcmFile);
    }if(mp3File) {
        fclose(mp3File);
        lame_close(lameClient);
    }
}複制代碼      

配置Cmake

打開CPP目錄下的CMakeList.txt檔案,向其中添加如下代碼

// 引入目錄
include_directories(libmp3lame)
// 将libmp3lame下所有檔案路徑指派給 SRC_LIST
aux_source_directory(libmp3lame SRC_LIST)

// 加入libmp3lame所有c檔案
add_library(mp3encoder
        SHARED
        mp3-encoder.cpp ${SRC_LIST})複制代碼      

并且向

target_link_libraries

添加

mp3encoder

target_link_libraries( 
        mp3encoder
        native-lib${log-lib})複制代碼      

修改CMakeList.txt之後,點選右上角

Sync Now

就可以看到我們

mp3-encoder.cpp

mp3-encoder.h

中的錯誤提示不見了,至此已基本完成

然後在我們的代碼中調用

Mp3Encoder

中的方法就可以将

PCM

編碼成

Mp3

private fun encodeAudio() {
    var pcmPath = File(externalCacheDir, "record.pcm").absolutePath
    var target = File(externalCacheDir, "target.mp3").absolutePath
    var encoder = Mp3Encoder()if (!File(pcmPath).exists()) {
        Toast.makeText(this, "請先進行錄制PCM音頻", Toast.LENGTH_SHORT).show()return}
    var ret = encoder.init(pcmPath, 2, 128, 44100, target)if (ret == 0) {
        encoder.encode()
        encoder.destroy()
        Toast.makeText(this, "PCM->MP3編碼完成", Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(this, "Lame初始化失敗", Toast.LENGTH_SHORT).show()
    }
}複制代碼      

【完整代碼-LameNative】

ndk-build編譯Lame

ndk-build編譯Lame,其實就是生成一個.so字尾的動态檔案庫供大家使用

  • 首先在任何目錄下建立

    jni

    檔案夾
  • 将上面Android項目中cpp目錄下修改好的libmp3lame、mp3-encoder.cpp和mp3-encoder.h拷貝至

    jni

  • Android.mk

    檔案

其中有幾個重要配置說明如下

· LOCAL_PATH:=$(call my-dir),傳回目前檔案在系統中的路徑,Android.mk檔案開始時必須定義該變量。

· include$(CLEAR_VARS),表明清除上一次建構過程的所有全局變量,因為在一個Makefile編譯腳本中,會使用大量的全局變量,使用這行腳本表明需要清除掉所有的全局變量

· LOCAL_MODULE,編譯目标項目名,如果是so檔案,則結果會以lib項目名.so呈現

· LOCAL_SRC_FILES,要編譯的C或者Cpp的檔案,注意這裡不需要列舉頭檔案,建構系統會自動幫助開發者依賴這些檔案。

· LOCAL_LDLIBS,所依賴的NDK動态和靜态庫。

· Linclude $(BUILD_SHARED_LIBRARY),建構動态庫

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := mp3encoder

LOCAL_SRC_FILES := mp3-encoder.cpp \
                 libmp3lame/bitstream.c \
                 libmp3lame/psymodel.c \
                 libmp3lame/lame.c \
                 libmp3lame/takehiro.c \
                 libmp3lame/encoder.c \
                 libmp3lame/quantize.c \
                 libmp3lame/util.c \
                 libmp3lame/fft.c \
                 libmp3lame/quantize_pvt.c \
                 libmp3lame/vbrquantize.c \
                 libmp3lame/gain_analysis.c \
                 libmp3lame/reservoir.c \
                 libmp3lame/VbrTag.c \
                 libmp3lame/mpglib_interface.c \
                 libmp3lame/id3tag.c \
                 libmp3lame/newmdct.c \
                 libmp3lame/set_get.c \
                 libmp3lame/version.c \
                 libmp3lame/presets.c \
                 libmp3lame/tables.c \

LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid -lm -pthread -L$(SYSROOT)/usr/lib

include $(BUILD_SHARED_LIBRARY)複制代碼      
  • Application.mk

APP_ABI := all 
APP_PLATFORM := android-21
APP_OPTIM := release
APP_STL := c++_static複制代碼      

最終效果如下:

最後在目前目錄下以command指令運作

ndk-build

/home/relo/Android/Sdk/ndk-bundle/ndk-build複制代碼      

如果不出意外,就可以在

jni

同級目錄

libs

下面看到各個平台的so檔案

将so檔案拷貝至我們普通Android項目jniLibs下面,然後在自己的包下(我這裡是

com.coder.media

),建立如上

Mp3Encoder

的檔案,最後在需要使用編碼MP3的位置使用

Mp3Encoder

中的三個方法就可以了。

但是需要注意的是需要在app下的build.gradle配置與jniLibs下對應的APP_ABI

參考

繼續閱讀