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

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

前景提要

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

  • 非壓縮編碼:音頻裸數據,也便是咱們所說的PCM
  • 壓縮編碼:對數據進行壓縮,壓縮不能被人耳感知到的冗餘信號

由於非壓縮編碼實在是太大了,因此咱們生活中所接觸的音頻編碼格式都是壓縮編碼,並且是有損壓縮,好比 MP3或AAC。
那如何操做PCM數據呢?Android SDK中提供了一套對PCM操做的API:AudioRecordAudioTrackc++

因爲AudioRecord(錄音)AudioTrack(播放)操做過於底層並且過於複雜,因此Android SDK 還提供了一套與之對應更加高級的API:MediaRecorder(錄音)MediaPlayer(播放),用於音視頻的操做,固然其更加簡單方便。咱們這裏只介紹前者,經過它來實現對PCM數據的操做。git

對於壓縮編碼,咱們則經過MediaCodecLame來分別實現AAC音頻和Mp3音頻壓縮編碼。話很少說,請往下看!github

AudioRecord

因爲AudioRecord更加底層,可以更好的而且直接的管理經過音頻錄製硬件設備錄製後的PCM數據,因此對數據處理更加靈活,可是同時也須要咱們本身處理編碼的過程。算法

AudioRecord的使用流程大體以下:緩存

  • 根據音頻參數建立AudioRecord
  • 調用startRecording開始錄製
  • 開啓錄製線程,經過AudioRecord將錄製的音頻數據從緩存中讀取並寫入文件
  • 釋放資源

在使用AudioRecord前須要先注意添加RECORD_AUDIO錄音權限。bash

建立AudioRecord

咱們先看看AudioRecord構造方法session

public AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes) 複製代碼
  • audioSource,從字面意思可知音頻來源,由MediaRecorder.AudioSource提供,主要有如下內容app

    · 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(立體雙聲道)

  • audioFormat,採樣格式,可選值以常量的形式定義在類AudioFormat中,分別爲ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),通常採用16bit。

  • 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判斷AudioRecord是否初始化成功,而後經過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.STARTING

    while ({ 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提供的AudioRecordAudioTrack對PCM數據進行操做。

可是僅僅這樣是不夠的,由於咱們生活中確定不是使用PCM進行音樂播放,那麼怎麼才能讓音頻在主流播放器上播放呢?這就須要咱們進行壓縮編碼了,好比mp3或aac壓縮編碼格式。

【完整代碼-AudioTrack】

MediaCodec編碼AAC

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

方便咱們處理AAC編碼,Android SDK中提供了MediaCodecAPI,能夠將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: Int
    while ({ 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 = this@AACEncoder.inputBuffers!![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 =this@AACEncoder.outputBuffers!![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、x8六、MIPS,其中比較經常使用的是ARM和x86。不管是哪一個平臺都會提供如下工具:

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

    ·AS:將彙編文件生成目標文件(彙編文件使用的是指令助記符,AS將它翻譯成機器碼)。

    ·AR:打包器,用於庫操做,能夠經過該工具從一個庫中刪除或者增長目標代碼模塊。

    ·LD:連接器,爲前面生成的目標代碼分配地址空間,將多個目標文件連接成一個庫或者是可執行文件。

    ·GDB:調試工具,能夠對運行過程當中的程序進行代碼調試工做。

    ·STRIP:以最終生成的可執行文件或者庫文件做爲輸入,而後消除掉其中的源碼。

    ·NM:查看靜態庫文件中的符號表。

    ·Objdump:查看靜態庫或者動態庫的方法簽名。

瞭解Android NDK 以後,就可新建一個支持C/C++ 的Android項目了:

  • 在嚮導的 Choose your project 部分中,選擇 Native C++ 項目類型。
  • 點擊 Next。
  • 填寫嚮導下一部分中的全部其餘字段。
  • 點擊 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_STRCHRHAVE_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 ): Int

    external 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 JNICALL Java_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 JNICALL Java_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.cppmp3-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

【完整代碼-ndk-build】

到此音頻非壓縮編碼和壓縮編碼基本講解完畢了,若有不明白或者不正確的地方,請在下方評論區留言,望共勉之。

參考

音視頻開發進階指南:基於Android與iOS平臺的實踐

相關文章
相關標籤/搜索