• 當前位置:首頁 > IT技術 > 移動平臺 > 正文

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼
    2021-09-24 14:45:43

    音視頻在開發中,最重要也是最復雜的就是編解碼的過程,我們說音頻的編碼根據大小劃分有兩種:壓縮編碼和非壓縮編碼,那到底是怎么實現的這兩中編碼的呢?這一次就詳細了解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??進行播放看看是不是剛剛我們錄制的聲音了。

    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提供的??AudioRecord??和??AudioTrack??對PCM數據進行操作。

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

    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: 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壓縮編碼的音頻文件,可以聽一聽是不是自己剛剛錄制的。我聽了一下我自己唱的一首歌,覺得我的還是可以的嘛,也不是那么五音不全~~

    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 應用安裝

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_音視頻

    在應用以上選項之后,我們可以看到SDK的目錄中多了一個??ndk-bundle??的文件夾,大致目錄結構如下

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_移動開發_02

    • 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。
    • 填寫向導下一部分中的所有其他字段。
    • 點擊 Next。
    • 在向導的 Customize C++ Support 部分中,您可以使用 C++ Standard 字段來自定義項目。使用下拉列表選擇您想要使用哪種 C++ 標準化。選擇 Toolchain Default 可使用默認的 CMake 設置。
    • 點擊 Finish,同步完成之后會出現如下圖所示的目錄結構,即表示原生項目創建完成

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_android_03

    編譯Lame

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

    • Cmake
    • ndk-build

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

    Cmake編譯Lame

    配置Cmake之后可以直接將Lame代碼運行于Android中

    準備

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

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_c++_04

    然后將里面的??libmp3lame??文件夾拷貝到我們上面創建的支持c/c++項目,刪除其中的i386和vector文件夾,以及其他非.c 和 .h 后綴的文件

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_移動開發_05

    需要將以下文件進行修改,否則會報錯

    • 將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
    ): 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.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()
    }
    }

    ndk-build編譯Lame

    ndk-build編譯Lame,其實就是生成一個.so后綴的動態文件庫供大家使用

    • 首先在任何目錄下創建??jni??文件夾

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_c++_06

    • 將上面Android項目中cpp目錄下修改好的libmp3lame、mp3-encoder.cpp和mp3-encoder.h拷貝至??jni??下

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_音視頻_07

    • 創建??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

    最終效果如下:

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_移動開發_08

    最后在當前目錄下以command命令運行??ndk-build??

    /home/relo/Android/Sdk/ndk-bundle/ndk-build

    如果不出意外,就可以在??jni??同級目錄??libs??下面看到各個平臺的so文件

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_移動開發_09

    將so文件拷貝至我們普通Android項目jniLibs下面,然后在自己的包下(我這里是??com.coder.media??),新建如上??Mp3Encoder??的文件,最后在需要使用編碼MP3的位置使用??Mp3Encoder??中的三個方法就可以了。

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_android_10

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

    Android音視頻開發之——音頻非壓縮編碼和壓縮編碼_移動開發_11

    到此音頻非壓縮編碼和壓縮編碼基本講解完畢了。

    本文摘自 :https://blog.51cto.com/u

    開通會員,享受整站包年服務
    国产呦精品一区二区三区网站|久久www免费人咸|精品无码人妻一区二区|久99久热只有精品国产15|中文字幕亚洲无线码