現在的短視頻、音視頻通話都離不開編碼和解碼,今天就來聊一下音頻的編解碼。

1. 音頻的基本概念

在音頻開發中,有些基本概念是需要了解的。

  • 採樣率(SampleRate):每秒採集聲音的數量,它用赫茲(Hz)來表示。採樣頻率越高,音頻質量越好。常用的音頻採樣頻率有:8kHz、16kHz、44.1kHz、48kHz 等。
  • 聲道數(Channel):一般表示聲音錄製時的音源數量或回放時相應的揚聲器數量。常用的是單聲道(Mono)和雙聲道(Stereo)。
  • 採樣精度(BitDepth):每個採樣點用多少數據量表示,它以位(Bit)為單位。位數越多,表示得就越精細,聲音質量自然就越好,當然數據量也越大。常見的位寬是:8bit 或者 16bit。
  • 比特率(BitRate):每秒音頻佔用的比特數量,單位是 bps(Bit Per Second),比特率越高,壓縮比越小,聲音質量越好,音頻體積也越大。

AAC 是應用非常廣泛的音頻壓縮格式,Android 硬體編碼天生支持 AAC。我們採集的原始 PCM 音頻,一般不直接用來網路傳輸,而是經過編碼器壓縮成 AAC,這樣就提高了傳輸效率,節省了網路帶寬。

簡言之,編碼就是壓縮,解碼就是解壓。編碼的目的是減小數據的體積,方便網路傳輸和本地存儲。編碼後的數據是不能直接使用的,必須先解碼成原來的樣子。就像 zip 壓縮文件裡面有張圖片,我們用圖片查看器是無法打開的,必須先解壓文件,恢復圖片原來的數據,這樣才能查看。音視頻編解碼也是同樣的道理。

2. MediaCodec 介紹

Android 在 API 16 後引入的音視頻編解碼 API,Android 應用層統一由 MediaCodec API 提供音視頻編解碼的功能,由參數配置來決定採用何種編解碼演算法、是否採用硬體編解碼加速等。由於使用硬體編解碼,兼容性有不少問題,據說 MediaCodec 坑比較多。

MediaCodec 採用了基於環形緩衝區的「生產者-消費者」模型,非同步處理數據。在 input 端,Client 是這個環形緩衝區「生產者」,MediaCodec 是「消費者」。在 output 端,MediaCodec 是這個環形緩衝區「生產者」,而 Client 則變成了「消費者」。

工作流程是這樣的:

(1)Client 從 input 緩衝區隊列申請 empty buffer [dequeueInputBuffer]

(2)Client 把需要編解碼的數據拷貝到 empty buffer,然後放入 input 緩衝區隊列 [queueInputBuffer]

(3)MediaCodec 從 input 緩衝區隊列取一幀數據進行編解碼處理

(4)處理結束後,MediaCodec 將原始數據 buffer 置為 empty 後放回 input 緩衝區隊列,將編解碼後的數據放入到 output 緩衝區隊列

(5)Client 從 output 緩衝區隊列申請編解碼後的 buffer [dequeueOutputBuffer]

(6)Client 對編解碼後的 buffer 進行渲染/播放

(7)渲染/播放完成後,Client 再將該 buffer 放回 output 緩衝區隊列 [releaseOutputBuffer]

MediaCodec 基本使用流程:

- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
- dequeueInputBuffer
- queueInputBuffer
- dequeueOutputBuffer
- releaseOutputBuffer
}
- stop
- release

3. 實時採集音頻並編碼

我們將使用 AudioRecord 和 MediaCodec 實現這個功能,關於 AudioRecord 的使用可以參考之前的文章:Android 音視頻開發 - 使用AudioRecord採集音頻。

為了保證兼容性,推薦的配置是 44.1kHz、單通道、16 位精度。首先創建並配置 AudioRecord 和 MediaCodec。

// 輸入源 麥克風
private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
// 採樣率 44.1kHz,所有設備都支持
private final static int SAMPLE_RATE = 44100;
// 通道 單聲道,所有設備都支持
private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
// 精度 16 位,所有設備都支持
private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
// 通道數 單聲道
private static final int CHANNEL_COUNT = 1;
// 比特率
private static final int BIT_RATE = 96000;

public void createAudio() {
mBufferSizeInBytes = AudioRecord.getMinBufferSize(AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT);
if (mBufferSizeInBytes <= 0) {
throw new RuntimeException("AudioRecord is not available, minBufferSize: " + mBufferSizeInBytes);
}
Log.i(TAG, "createAudioRecord minBufferSize: " + mBufferSizeInBytes);

mAudioRecord = new AudioRecord(AudioEncoder.AUDIO_SOURCE, AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT, mBufferSizeInBytes);
int state = mAudioRecord.getState();
Log.i(TAG, "createAudio state: " + state + ", initialized: " + (state == AudioRecord.STATE_INITIALIZED));
}

public void createMediaCodec() throws IOException {
MediaCodecInfo mediaCodecInfo = CodecUtils.selectCodec(MIMETYPE_AUDIO_AAC);
if (mediaCodecInfo == null) {
throw new RuntimeException(MIMETYPE_AUDIO_AAC + " encoder is not available");
}
Log.i(TAG, "createMediaCodec: mediaCodecInfo " + mediaCodecInfo.getName());

MediaFormat format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);

mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}

然後開始錄音,得到原始音頻數據,再編碼為 AAC 格式。這個地方會阻塞調用的線程,而且編碼比較耗時,一定要在主線程之外調用。

public void start(File outFile) throws IOException {
Log.d(TAG, "start() called with: outFile = [" + outFile + "]");
mStopped = false;
FileOutputStream fos = new FileOutputStream(outFile);
mMediaCodec.start();
mAudioRecord.startRecording();
byte[] buffer = new byte[mBufferSizeInBytes];
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
try {
while (!mStopped) {
int readSize = mAudioRecord.read(buffer, 0, mBufferSizeInBytes);
if (readSize > 0) {
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(buffer);
inputBuffer.limit(buffer.length);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, readSize, 0, 0);
}

MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
byte[] chunkAudio = new byte[bufferInfo.size + 7];// 7 is ADTS size
addADTStoPacket(chunkAudio, chunkAudio.length);
outputBuffer.get(chunkAudio, 7, bufferInfo.size);
outputBuffer.position(bufferInfo.offset);
fos.write(chunkAudio);
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
} else {
Log.w(TAG, "read audio buffer error:" + readSize);
break;
}
}
} finally {
Log.i(TAG, "released");
mAudioRecord.stop();
mAudioRecord.release();
mMediaCodec.stop();
mMediaCodec.release();
fos.close();
}
}

AAC 是一種壓縮格式,可以直接使用播放器播放。為了實現流式播放,也就是做到邊下邊播,我們採用 ADTS 格式。給每幀加上 7 個位元組的頭信息。加上頭信息就是為了告訴解碼器,這幀音頻長度、採樣率、通道是多少,每幀都攜帶頭信息,解碼器隨時都可以解碼播放。我們這裡採用單通道、44.1KHz 採樣率的頭信息配置。

private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
int freqIdx = 4; //44.1KHz
int chanCfg = 1; //CPE
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}

4. AAC 解碼

我們可以利用 MediaExtractor 和 MediaCodec 來提取編碼後的音頻數據,並解壓成音頻源數據。

/**
* AAC 格式解碼成 PCM 數據
*
* @param aacFile
* @param pcmFile
* @throws IOException
*/
public static void decodeAacToPcm(File aacFile, File pcmFile) throws IOException {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(aacFile.getAbsolutePath());
MediaFormat mediaFormat = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
extractor.selectTrack(i);
mediaFormat = format;
break;
}
}
if (mediaFormat == null) {
Log.e(TAG, "Invalid file with audio track.");
extractor.release();
return;
}

FileOutputStream fosDecoder = new FileOutputStream(pcmFile);
String mediaMime = mediaFormat.getString(MediaFormat.KEY_MIME);
Log.i(TAG, "decodeAacToPcm: mimeType: " + mediaMime);
MediaCodec codec = MediaCodec.createDecoderByType(mediaMime);
codec.configure(mediaFormat, null, null, 0);
codec.start();
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
final long kTimeOutUs = 10_000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;

try {
while (!sawOutputEOS) {
if (!sawInputEOS) {
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
int sampleSize = extractor.readSampleData(dstBuf, 0);
if (sampleSize < 0) {
Log.i(TAG, "saw input EOS.");
sawInputEOS = true;
codec.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(inputBufIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
}

int outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (outputBufferIndex >= 0) {
// Simply ignore codec config buffers.
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.i(TAG, "audio encoder: codec config buffer");
codec.releaseOutputBuffer(outputBufferIndex, false);
continue;
}

if (info.size != 0) {
ByteBuffer outBuf = codecOutputBuffers[outputBufferIndex];
outBuf.position(info.offset);
outBuf.limit(info.offset + info.size);
byte[] data = new byte[info.size];
outBuf.get(data);
fosDecoder.write(data);
}

codec.releaseOutputBuffer(outputBufferIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "saw output EOS.");
sawOutputEOS = true;
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
Log.i(TAG, "output buffers have changed.");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat oformat = codec.getOutputFormat();
Log.i(TAG, "output format has changed to " + oformat);
}
}
} finally {
Log.i(TAG, "decodeAacToPcm finish");
codec.stop();
codec.release();
extractor.release();
fosDecoder.close();
}
}

源碼在 GitHub上,歡迎交流。

自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?

沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【包括高級UI、性能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。

喜歡我的文章可以點贊+關注我的【個人主頁】獲取免費資料,後續我將繼續分享更多Android技術乾貨,感謝支持!

資料大全

推薦閱讀:
相關文章