當(dāng)前位置:首頁(yè) > IT技術(shù) > 移動(dòng)平臺(tái) > 正文

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼
2021-09-24 14:45:43

音視頻在開(kāi)發(fā)中,最重要也是最復(fù)雜的就是編解碼的過(guò)程,我們說(shuō)音頻的編碼根據(jù)大小劃分有兩種:壓縮編碼和非壓縮編碼,那到底是怎么實(shí)現(xiàn)的這兩中編碼的呢?這一次就詳細(xì)了解Android中如何使用這兩種方式進(jìn)行音頻編碼

前景提要

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

  • 非壓縮編碼:音頻裸數(shù)據(jù),也即是我們所說(shuō)的PCM
  • 壓縮編碼:對(duì)數(shù)據(jù)進(jìn)行壓縮,壓縮不能被人耳感知到的冗余信號(hào)

因?yàn)榉菈嚎s編碼實(shí)在是太大了,所以我們生活中所接觸的音頻編碼格式都是壓縮編碼,而且是有損壓縮,比如 MP3或AAC。

那如何操作PCM數(shù)據(jù)呢?Android SDK中提供了一套對(duì)PCM操作的API:??AudioRecord?? 和 ??AudioTrack??;

由于??AudioRecord(錄音)?? 和 ??AudioTrack(播放)??操作過(guò)于底層而且過(guò)于復(fù)雜,所以Android SDK 還提供了一套與之對(duì)應(yīng)更加高級(jí)的API:??MediaRecorder(錄音)??和??MediaPlayer(播放)??,用于音視頻的操作,當(dāng)然其更加簡(jiǎn)單方便。我們這里只介紹前者,通過(guò)它來(lái)實(shí)現(xiàn)對(duì)PCM數(shù)據(jù)的操作。

對(duì)于壓縮編碼,我們則通過(guò)??MediaCodec??和??Lame??來(lái)分別實(shí)現(xiàn)AAC音頻和Mp3音頻壓縮編碼。話不多說(shuō),請(qǐng)往下看!

AudioRecord

由于??AudioRecord??更加底層,能夠更好的并且直接的管理通過(guò)音頻錄制硬件設(shè)備錄制后的PCM數(shù)據(jù),所以對(duì)數(shù)據(jù)處理更加靈活,但是同時(shí)也需要我們自己處理編碼的過(guò)程。

AudioRecord的使用流程大致如下:

  • 根據(jù)音頻參數(shù)創(chuàng)建??AudioRecord??
  • 調(diào)用??startRecording??開(kāi)始錄制
  • 開(kāi)啟錄制線程,通過(guò)??AudioRecord??將錄制的音頻數(shù)據(jù)從緩存中讀取并寫(xiě)入文件
  • 釋放資源

在使用??AudioRecord??前需要先注意添加??RECORD_AUDIO??錄音權(quán)限。

創(chuàng)建AudioRecord

我們先看看??AudioRecord??構(gòu)造方法

public AudioRecord (int audioSource, 
int sampleRateInHz,
int channelConfig,
int audioFormat,
int bufferSizeInBytes)


  • audioSource,從字面意思可知音頻來(lái)源,由MediaRecorder.AudioSource提供,主要有以下內(nèi)容
    · CAMCORDER 與照相機(jī)方向相同的麥克風(fēng)音頻源
    · DEFAULT 默認(rèn)
    · MIC 麥克風(fēng)音頻源
    · VOICE_CALL 語(yǔ)音通話
    這里采用MIC麥克風(fēng)音頻源
  • sampleRateInHz,采樣率,即錄制的音頻每秒鐘會(huì)有多少次采樣,可選用的采樣頻率列表為:8000、16000、22050、24000、32000、44100、48000等,一般采用人能聽(tīng)到最大音頻的2倍,也就是44100Hz。
  • channelConfig,聲道數(shù)的配置,可選值以常量的形式配置在類(lèi)AudioFormat中,常用的是CHANNEL_IN_MONO(單聲道)、CHANNEL_IN_STEREO(雙聲道)
  • audioFormat,采樣格式,可選值以常量的形式定義在類(lèi)AudioFormat中,分別為ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),一般采用16bit。
  • bufferSizeInBytes,其配置的是AudioRecord內(nèi)部的音頻緩沖區(qū)的大小,可能會(huì)因?yàn)樯a(chǎn)廠家的不同而有所不同,為了方便AudioRecord提供了一個(gè)獲取該值最小緩沖區(qū)大小的方法getMinBufferSize。
public static int getMinBufferSize (int sampleRateInHz, 
int channelConfig,
int audioFormat)

在開(kāi)發(fā)過(guò)程中需使用??getMinBufferSize??此方法計(jì)算出最小緩存大小。

切換錄制狀態(tài)

首先通過(guò)調(diào)用??getState??判斷AudioRecord是否初始化成功,然后通過(guò)??startRecording??切換成錄制狀態(tài)

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

開(kāi)啟錄制線程

thread = Thread(Runnable {
writeData2File()
})
thread?.start()

開(kāi)啟錄音線程將錄音數(shù)據(jù)通過(guò)AudioRecord寫(xiě)入文件

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
}

通過(guò)以上一個(gè)流程之后,就可以得到一個(gè)非壓縮編碼的PCM數(shù)據(jù)了。

但是這個(gè)數(shù)據(jù)在音樂(lè)播放器上一般是播放不了的,那么怎么驗(yàn)證我是否錄制成功呢?當(dāng)然是使用我們的??AudioTrack??進(jìn)行播放看看是不是剛剛我們錄制的聲音了。

AudioTrack

由于??AudioTrack??是由Android SDK提供比較底層的播放API,也只能操作PCM裸數(shù)據(jù),通過(guò)直接渲染PCM數(shù)據(jù)進(jìn)行播放。當(dāng)然如果想要使用??AudioTrack??進(jìn)行播放,那就需要自行先將壓縮編碼格式文件解碼。

AudioTrack的使用流程大致如下:

  • 根據(jù)音頻參數(shù)創(chuàng)建??AudioTrack??
  • 調(diào)用??play??開(kāi)始播放
  • 開(kāi)啟播放線程,循環(huán)想??AudioTrack??緩存區(qū)寫(xiě)入音頻數(shù)據(jù)
  • 釋放資源

創(chuàng)建AudioTrack

我們來(lái)看看??AudioTrack??的構(gòu)造方法

public AudioTrack (int streamType, 
int sampleRateInHz,
int channelConfig,
int audioFormat,
int bufferSizeInBytes,
int mode,
int sessionId)

  • streamType,Android手機(jī)上提供音頻管理策略,按下音量鍵我們會(huì)發(fā)現(xiàn)由媒體聲音管理,鬧鈴聲音管理,通話聲音管理等等,當(dāng)系統(tǒng)有多個(gè)進(jìn)程需要播放音頻的時(shí)候,管理策略會(huì)決定最終的呈現(xiàn)效果,該參數(shù)的可選值將以常量的形式定義在類(lèi)AudioManager中,主要包括以下內(nèi)容:
    · STREAM_VOCIE_CALL:電話聲音
    · STREAM_SYSTEM:系統(tǒng)聲音
    · STREAM_RING:鈴聲
    · STREAM_MUSCI:音樂(lè)聲
    · STREAM_ALARM:警告聲
    · STREAM_NOTIFICATION:通知聲

因?yàn)檫@里是播放音頻,所以我們選擇??STREAM_MUSCI??。

  • sampleRateInHz,采樣率,即播放的音頻每秒鐘會(huì)有多少次采樣,可選用的采樣頻率列表為:8000、16000、22050、24000、32000、44100、48000等,一般采用人能聽(tīng)到最大音頻的2倍,也就是44100Hz。
  • channelConfig,聲道數(shù)的配置,可選值以常量的形式配置在類(lèi)AudioFormat中,常用的是CHANNEL_IN_MONO(單聲道)、CHANNEL_IN_STEREO(立體雙聲道)
  • audioFormat,采樣格式,可選值以常量的形式定義在類(lèi)AudioFormat中,分別為ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),一般采用16bit。
  • bufferSizeInBytes,其配置的是AudioTrack內(nèi)部的音頻緩沖區(qū)的大小,可能會(huì)因?yàn)樯a(chǎn)廠家的不同而有所不同,為了方便AudioTrack提供了一個(gè)獲取該值最小緩沖區(qū)大小的方法getMinBufferSize。
  • mode,播放模式,AudioTrack提供了兩種播放模式,可選的值以常量的形式定義在類(lèi)AudioTrack中,一個(gè)是MODE_STATIC,需要一次性將所有的數(shù)據(jù)都寫(xiě)入播放緩沖區(qū)中,簡(jiǎn)單高效,通常用于播放鈴聲、系統(tǒng)提醒的音頻片段;另一個(gè)是MODE_STREAM,需要按照一定的時(shí)間間隔不間斷地寫(xiě)入音頻數(shù)據(jù),理論上它可以應(yīng)用于任何音頻播放的場(chǎng)景。
  • sessionId,AudioTrack都需要關(guān)聯(lián)一個(gè)會(huì)話Id,在創(chuàng)建AudioTrack時(shí)可直接使用AudioManager.AUDIO_SESSION_ID_GENERATE,或者在構(gòu)造之前通過(guò)AudioManager.generateAudioSessionId獲取。

上面這種構(gòu)造方法已經(jīng)被棄用了,現(xiàn)在基本使用如下構(gòu)造(最小skd 版本需要>=21),參數(shù)內(nèi)容與上基本一致:

public AudioTrack (AudioAttributes attributes, 
AudioFormat format,
int bufferSizeInBytes,
int mode,
int sessionId)

通過(guò)??AudioAttributes.Builder??設(shè)置參數(shù)streamType

var audioAttributes = AudioAttributes.Builder()
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build()

通過(guò)??AudioFormat.Builder??設(shè)置channelConfig,sampleRateInHz,audioFormat參數(shù)

var mAudioFormat = AudioFormat.Builder()
.setChannelMask(channel)
.setEncoding(audioFormat)
.setSampleRate(sampleRate)
.build()

切換播放狀態(tài)

首先通過(guò)調(diào)用??getState??判斷AudioRecord是否初始化成功,然后通過(guò)??play??切換成錄播放狀態(tài)

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

開(kāi)啟播放線程

開(kāi)啟播放線程

thread= Thread(Runnable {
readDataFromFile()
})
thread?.start()

將數(shù)據(jù)不斷的送入緩存區(qū)并通過(guò)AudioTrack播放

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

val file = File(externalCacheDir?.absolutePath + File.separator + filename)
if (!file.exists()) {
Toast.makeText(this, "請(qǐng)先進(jìn)行錄制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
}

經(jīng)過(guò)這樣幾個(gè)步驟,我們就可以聽(tīng)到剛剛我們錄制的PCM數(shù)據(jù)聲音啦!這就是使用Android提供的??AudioRecord??和??AudioTrack??對(duì)PCM數(shù)據(jù)進(jìn)行操作。

但是僅僅這樣是不夠的,因?yàn)槲覀兩钪锌隙ú皇鞘褂肞CM進(jìn)行音樂(lè)播放,那么怎么才能讓音頻在主流播放器上播放呢?這就需要我們進(jìn)行壓縮編碼了,比如mp3或aac壓縮編碼格式。

MediaCodec編碼AAC

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

方便我們處理AAC編碼,Android SDK中提供了??MediaCodec??API,可以將PCM數(shù)據(jù)編碼成AAC數(shù)據(jù)。大概需要以下幾個(gè)步驟:

  • 創(chuàng)建??MediaCodec??
  • 為??MediaCodec??配置音頻參數(shù)
  • 啟動(dòng)線程,循環(huán)往緩沖區(qū)送入數(shù)據(jù)
  • 通過(guò)??MediaCodec??將緩沖區(qū)的數(shù)據(jù)進(jìn)行編碼并寫(xiě)入文件
  • 釋放資源

創(chuàng)建MediaCodec

通過(guò)??MediaCodec.createEncoderByType??創(chuàng)建編碼MediaCodec

mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)

配置音頻參數(shù)

// 配置采樣率和聲道數(shù)
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

啟動(dòng)線程

啟動(dòng)線程,循環(huán)讀取PCM數(shù)據(jù)送入緩沖區(qū)

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數(shù)據(jù)通過(guò)??MediaCodec??進(jìn)行編碼,大致流程如下:

  • 通過(guò)可用緩存去索引,獲取可用輸入緩沖區(qū)
  • 將pcm數(shù)據(jù)放入輸入緩沖區(qū)并提交
  • 根據(jù)輸出緩沖區(qū)索引,獲取輸出緩沖區(qū)
  • 創(chuàng)建輸出數(shù)據(jù)??data??,并添加ADTS頭部信息(有7byte)
  • 將??outputBuffer??編碼后數(shù)據(jù)寫(xiě)入??data??(data有7byte偏移)
  • 將編碼數(shù)據(jù)??data??寫(xiě)入文件
  • 重復(fù)以上過(guò)程
private fun encode(byteArray: ByteArray){
mediaCodec?.run {
//返回要用有效數(shù)據(jù)填充的輸入緩沖區(qū)的索引, -1 無(wú)限期地等待輸入緩沖區(qū)的可用性
val inputIndex = dequeueInputBuffer(-1)
if (inputIndex > 0){
// 根據(jù)索引獲取可用輸入緩存區(qū)
val inputBuffer = this@AACEncoder.inputBuffers!![inputIndex]
// 清空緩沖區(qū)
inputBuffer.clear()
// 將pcm數(shù)據(jù)放入緩沖區(qū)
inputBuffer.put(byteArray)
// 提交放入數(shù)據(jù)緩沖區(qū)索引以及大小
queueInputBuffer(inputIndex,0,byteArray.size,System.nanoTime(),0)
}
// 指定編碼器緩沖區(qū)中有效數(shù)據(jù)范圍
val bufferInfo = MediaCodec.BufferInfo()
// 獲取輸出緩沖區(qū)索引
var outputIndex = dequeueOutputBuffer(bufferInfo,0)

while (outputIndex>0){
// 根據(jù)索引獲取可用輸出緩存區(qū)
val outputBuffer =this@AACEncoder.outputBuffers!![outputIndex]
// 測(cè)量輸出緩沖區(qū)大小
val bufferSize = bufferInfo.size
// 輸出緩沖區(qū)實(shí)際大小,ADTS頭部長(zhǎng)度為7
val bufferOutSize = bufferSize+7

// 指定輸出緩存區(qū)偏移位置以及限制大小
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset+bufferSize)
// 創(chuàng)建輸出空數(shù)據(jù)
val data = ByteArray(bufferOutSize)
// 向空數(shù)據(jù)先增加ADTS頭部
addADTStoPacket(data, bufferOutSize)
// 將編碼輸出數(shù)據(jù)寫(xiě)入已加入ADTS頭部的數(shù)據(jù)中
outputBuffer.get(data,7,bufferInfo.size)
// 重新指定輸出緩存區(qū)偏移
outputBuffer.position(bufferInfo.offset)
// 將獲取的數(shù)據(jù)寫(xiě)入文件
fos?.write(data)
// 釋放輸出緩沖區(qū)
releaseOutputBuffer(outputIndex,false)
// 重新獲取輸出緩沖區(qū)索引
outputIndex=dequeueOutputBuffer(bufferInfo,0)
}
}
}

釋放資源

編碼完成后,一定要釋放所有資源,首先關(guān)閉輸入輸出流

fos?.close()
fis.close()

停止編碼

if (mediaCodec!=null){
mediaCodec?.stop()
}

然后就是關(guān)閉線程

if (thread!=null){
thread?.join()
thread =null
}

最后釋放MediaCodec

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

mediaFormat = null
inputBuffers = null
outputBuffers = null
}

通過(guò)以上一個(gè)流程,我們就可以得到一個(gè)AAC壓縮編碼的音頻文件,可以聽(tīng)一聽(tīng)是不是自己剛剛錄制的。我聽(tīng)了一下我自己唱的一首歌,覺(jué)得我的還是可以的嘛,也不是那么五音不全~~

Android NDK

雖然我們通過(guò)壓縮編碼生成了AAC音頻文件,但是有個(gè)問(wèn)題:畢竟AAC音頻不是主流的音頻文件呀,我們最常見(jiàn)的是MP3的嘛,可不可以將PCM編碼成MP3呢?

當(dāng)然是可以的,但是Android SDK沒(méi)有直接提供這樣的API,只能使用Android NDK,通過(guò)交叉編譯其他C或C++庫(kù)來(lái)進(jìn)行實(shí)現(xiàn)。

Android NDK 是由Google提供一個(gè)工具集,可讓您使用 C 和 C++ 等語(yǔ)言實(shí)現(xiàn)應(yīng)用。

Android NDK 一般有兩個(gè)用途,一個(gè)是進(jìn)一步提升設(shè)備性能,以降低延遲,或運(yùn)行計(jì)算密集型應(yīng)用,如游戲或物理模擬;另一個(gè)是重復(fù)使用您自己或其他開(kāi)發(fā)者的 C 或 C++ 庫(kù)。當(dāng)然我們使用最多的應(yīng)該還是后者。

想使用Android NDK調(diào)試代碼需要以下工具:

  • Android 原生開(kāi)發(fā)套件 (NDK):這套工具使您能在 Android 應(yīng)用中使用 C 和 C++ 代碼。
  • CMake:一款外部編譯工具,可與 Gradle 搭配使用來(lái)編譯原生庫(kù)。如果您只計(jì)劃使用 ndk-build,則不需要此組件。
  • LLDB:Android Studio 用于調(diào)試原生代碼的調(diào)試程序。

可以進(jìn)入Tools > SDK Manager > SDK Tools 選擇 NDK (Side by side) 和 CMake 應(yīng)用安裝

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_音視頻

在應(yīng)用以上選項(xiàng)之后,我們可以看到SDK的目錄中多了一個(gè)??ndk-bundle??的文件夾,大致目錄結(jié)構(gòu)如下

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_移動(dòng)開(kāi)發(fā)_02

  • ndk-build:該Shell腳本是Android NDK構(gòu)建系統(tǒng)的起始點(diǎn),一般在項(xiàng)目中僅僅執(zhí)行這一個(gè)命令就可以編譯出對(duì)應(yīng)的動(dòng)態(tài)鏈接庫(kù)了,后面的編譯mp3lame 就會(huì)使用到。
  • platforms:該目錄包含支持不同Android目標(biāo)版本的頭文件和庫(kù)文件,NDK構(gòu)建系統(tǒng)會(huì)根據(jù)具體的配置來(lái)引用指定平臺(tái)下的頭文件和庫(kù)文件。
  • toolchains:該目錄包含目前NDK所支持的不同平臺(tái)下的交叉編譯器——ARM、x86、MIPS,其中比較常用的是ARM和x86。不論是哪個(gè)平臺(tái)都會(huì)提供以下工具:
    ·CC:編譯器,對(duì)C源文件進(jìn)行編譯處理,生成匯編文件。
    ·AS:將匯編文件生成目標(biāo)文件(匯編文件使用的是指令助記符,AS將它翻譯成機(jī)器碼)。
    ·AR:打包器,用于庫(kù)操作,可以通過(guò)該工具從一個(gè)庫(kù)中刪除或者增加目標(biāo)代碼模塊。
    ·LD:鏈接器,為前面生成的目標(biāo)代碼分配地址空間,將多個(gè)目標(biāo)文件鏈接成一個(gè)庫(kù)或者是可執(zhí)行文件。
    ·GDB:調(diào)試工具,可以對(duì)運(yùn)行過(guò)程中的程序進(jìn)行代碼調(diào)試工作。
    ·STRIP:以最終生成的可執(zhí)行文件或者庫(kù)文件作為輸入,然后消除掉其中的源碼。
    ·NM:查看靜態(tài)庫(kù)文件中的符號(hào)表。
    ·Objdump:查看靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù)的方法簽名。

了解Android NDK 之后,就可新建一個(gè)支持C/C++ 的Android項(xiàng)目了:

  • 在向?qū)У?Choose your project 部分中,選擇 Native C++ 項(xiàng)目類(lèi)型。
  • 點(diǎn)擊 Next。
  • 填寫(xiě)向?qū)乱徊糠种械乃衅渌侄巍?/li>
  • 點(diǎn)擊 Next。
  • 在向?qū)У?Customize C++ Support 部分中,您可以使用 C++ Standard 字段來(lái)自定義項(xiàng)目。使用下拉列表選擇您想要使用哪種 C++ 標(biāo)準(zhǔn)化。選擇 Toolchain Default 可使用默認(rèn)的 CMake 設(shè)置。
  • 點(diǎn)擊 Finish,同步完成之后會(huì)出現(xiàn)如下圖所示的目錄結(jié)構(gòu),即表示原生項(xiàng)目創(chuàng)建完成

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_android_03

編譯Lame

LAME是一個(gè)開(kāi)源的MP3音頻壓縮庫(kù),當(dāng)前是公認(rèn)有損質(zhì)量MP3中壓縮效果最好的編碼器,所以我們選擇它來(lái)進(jìn)行壓縮編碼,那如何進(jìn)行壓縮編碼呢?主流的由兩種方式:

  • Cmake
  • ndk-build

下面就詳細(xì)講解這兩種方式

Cmake編譯Lame

配置Cmake之后可以直接將Lame代碼運(yùn)行于Android中

準(zhǔn)備

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

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_c++_04

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

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_移動(dòng)開(kāi)發(fā)_05

需要將以下文件進(jìn)行修改,否則會(huì)報(bào)錯(cuò)

  • 將util.h中570行
extern ieee754_float32_t fast_log2(ieee754_float32_t x)

替換成

extern float fast_log2(float x)

  • 在id3tag.c和machine.h兩個(gè)文件中,將??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"

編寫(xiě)Mp3編碼器

首先在自己的包下(我這里是??com.coder.media??,這個(gè)很重要,后面會(huì)用到),新建??Mp3Encoder??的文件,大概如下幾個(gè)方法

  • init,將聲道,比特率,采樣率等信息傳入
  • encode,根據(jù)init中提供的信息進(jìn)行編碼
  • 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目錄下新建兩個(gè)文件

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

這兩個(gè)文件中可能會(huì)提示錯(cuò)誤異常,先不要管它,這是因?yàn)槲覀冞€沒(méi)有配置??CMakeList.txt??導(dǎo)致的。

在??mp3-encoder.h??中定義三個(gè)變量

FILE* pcmFile;
FILE* mp3File;
lame_t lameClient;

然后在??mp3-encoder.c??中分別實(shí)現(xiàn)我們?cè)??Mp3Encoder??中定義的三個(gè)方法

首先導(dǎo)入需要的文件

#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__)

然后實(shí)現(xiàn)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;
}

這個(gè)方法的作用就是將我們的音頻參數(shù)信息送入??lameClient??

需要注意我這里的方法??Java_com_coder_media_Mp3Encoder_init??中的??com_coder_media??需要替換成你自己的對(duì)應(yīng)包名,下面的encode和destroy也是如此,切記?。?!

實(shí)現(xiàn)通過(guò)??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

打開(kāi)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之后,點(diǎn)擊右上角??Sync Now??就可以看到我們??mp3-encoder.cpp??和??mp3-encoder.h??中的錯(cuò)誤提示不見(jiàn)了,至此已基本完成

然后在我們的代碼中調(diào)用??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, "請(qǐng)先進(jìn)行錄制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,其實(shí)就是生成一個(gè).so后綴的動(dòng)態(tài)文件庫(kù)供大家使用

  • 首先在任何目錄下創(chuàng)建??jni??文件夾

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_c++_06

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

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_音視頻_07

  • 創(chuàng)建??Android.mk??文件

其中有幾個(gè)重要配置說(shuō)明如下

· LOCAL_PATH:=$(call my-dir),返回當(dāng)前文件在系統(tǒng)中的路徑,Android.mk文件開(kāi)始時(shí)必須定義該變量。

· include$(CLEAR_VARS),表明清除上一次構(gòu)建過(guò)程的所有全局變量,因?yàn)樵谝粋€(gè)Makefile編譯腳本中,會(huì)使用大量的全局變量,使用這行腳本表明需要清除掉所有的全局變量

· LOCAL_MODULE,編譯目標(biāo)項(xiàng)目名,如果是so文件,則結(jié)果會(huì)以lib項(xiàng)目名.so呈現(xiàn)

· LOCAL_SRC_FILES,要編譯的C或者Cpp的文件,注意這里不需要列舉頭文件,構(gòu)建系統(tǒng)會(huì)自動(dòng)幫助開(kāi)發(fā)者依賴(lài)這些文件。

· LOCAL_LDLIBS,所依賴(lài)的NDK動(dòng)態(tài)和靜態(tài)庫(kù)。

· Linclude $(BUILD_SHARED_LIBRARY),構(gòu)建動(dòng)態(tài)庫(kù)

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)

  • 創(chuàng)建??Application.mk??
APP_ABI := all 
APP_PLATFORM := android-21
APP_OPTIM := release
APP_STL := c++_static

最終效果如下:

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_移動(dòng)開(kāi)發(fā)_08

最后在當(dāng)前目錄下以command命令運(yùn)行??ndk-build??

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

如果不出意外,就可以在??jni??同級(jí)目錄??libs??下面看到各個(gè)平臺(tái)的so文件

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_移動(dòng)開(kāi)發(fā)_09

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

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_android_10

但是需要注意的是需要在app下的build.gradle配置與jniLibs下對(duì)應(yīng)的APP_ABI

Android音視頻開(kāi)發(fā)之——音頻非壓縮編碼和壓縮編碼_移動(dòng)開(kāi)發(fā)_11

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

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

開(kāi)通會(huì)員,享受整站包年服務(wù)立即開(kāi)通 >