Android AVDemo(2):音訊編碼,採集 PCM 資料編碼為 AAC丨音視訊工程示例

語言: CN / TW / HK

塞尚《靜物》

這個公眾號會 路線圖 式的遍歷分享音視訊技術 音視訊基礎(完成)  →  音視訊工具(完成)  →  音視訊工程示例(進行中)  →  音視訊工業實戰(準備) 關注一下成本不高,錯過乾貨損失不小 ↓↓↓

iOS/Android 客戶端開發同學如果想要開始學習音視訊開發,最絲滑的方式是對音視訊基礎概念知識有一定了解後,再借助 iOS/Android 平臺的音視訊能力上手去實踐音視訊的 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 過程,並藉助音視訊工具來分析和理解對應的音視訊資料。

在音視訊工程示例這個欄目,我們將通過拆解 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 流程並實現 Demo 來向大家介紹如何在 iOS/Android 平臺上手音視訊開發。

這裡是 Android 第二篇: Android 音訊編碼 Demo 。這個 Demo 裡包含以下內容:

  • 1)實現一個音訊採集模組;

  • 2)實現一個音訊編碼模組;

  • 3)串聯音訊採集和編碼模組,將採集到的音訊資料輸入給 AAC 編碼模組進行編碼和儲存;

  • 4)詳盡的程式碼註釋,幫你理解程式碼邏輯和原理;

1、音訊採集模組

在這個 Demo 中,音訊採集模組 KFAudioCapture 的實現與  Android 音訊採集 Demo 中一樣,這裡就不再重複介紹了,其介面如下:

KFAudioCapture.java

public class KFAudioCapture {
public KFAudioCapture(KFAudioCaptureConfig config,KFAudioCaptureListener listener);
public void startRunning(); ///< 開始採集音訊資料。
public void stopRunning(); ///< 停止採集音訊資料。
public void release(); ///< 釋放音訊採集。
}

2、音訊編碼模組

我們定義了介面類 KFMediaCodecInterface ,後續編解碼模組實現這個介面即可。需要關注  setup 介面的引數 isEncoder 代表是否使用編碼功能,mediaFormat 代表輸入資料格式描述。

KFMediaCodecInterface.java

public interface KFMediaCodecInterface {
public static final int KFMediaCodecInterfaceErrorCreate = -2000;
public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
public static final int KFMediaCodecInterfaceErrorStart = -2002;
public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
public static final int KFMediaCodecInterfaceErrorParams = -2004;

public static int KFMediaCodeProcessParams = -1;
public static int KFMediaCodeProcessAgainLater = -2;
public static int KFMediaCodeProcessSuccess = 0;

///< 初始化 Codec,第一個引數需告知使用編碼還是解碼。
public void setup(boolean isEncoder, MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
///< 釋放 Codec。
public void release();

///< 獲取輸出格式描述。
public MediaFormat getOutputMediaFormat();
///< 獲取輸入格式描述。
public MediaFormat getInputMediaFormat();
///< 處理每一幀資料,編碼前與編碼後都可以,支援編解碼 2 種模式。
public int processFrame(KFFrame frame);
///< 清空 Codec 緩衝區。
public void flush();
}

接下來,我們來實現一個音訊編碼模組 KFByteBufferCodec ,需要實現上面的介面  KFMediaCodecInterface  ,在這裡輸入採集後的資料,輸出編碼後的資料。這裡命名為 KFByteBufferCodec,主要因為它可以支援音視訊編解碼多個功能。

KFByteBufferCodec.java

public class KFByteBufferCodec implements KFMediaCodecInterface {
public static final int KFByteBufferCodecErrorParams = -2500;
public static final int KFByteBufferCodecErrorCreate = -2501;
public static final int KFByteBufferCodecErrorConfigure = -2502;
public static final int KFByteBufferCodecErrorStart = -2503;

private static final int KFByteBufferCodecInputBufferMaxCache = 20 * 1024 * 1024;
private static final String TAG = "KFByteBufferCodec";
private KFMediaCodecListener mListener = null; ///< 回撥
private MediaCodec mMediaCodec = null; ///< Codec 例項
private ByteBuffer[] mInputBuffers; ///< Codec 輸入緩衝區
private MediaFormat mInputMediaFormat = null; ///< 輸入資料格式描述
private MediaFormat mOutMediaFormat = null; ///< 輸出資料格式描述

private long mLastInputPts = 0; ///< 上一幀時間戳
private List<KFBufferFrame> mList = new ArrayList<>(); ///< 輸入資料快取
private int mListCacheSize = 0; ///< 輸入資料快取數量
private ReentrantLock mListLock = new ReentrantLock(true); ///< 資料快取鎖
private boolean mIsEncoder = true;

private HandlerThread mCodecThread = null; ///< Codec 執行緒
private Handler mCodecHandler = null;
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主執行緒

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext) {
mListener = listener;
mInputMediaFormat = mediaFormat;
mIsEncoder = isEncoder;

mCodecThread = new HandlerThread("KFByteBufferCodecThread");
mCodecThread.start();
mCodecHandler = new Handler((mCodecThread.getLooper()));

mCodecHandler.post(()->{
if(mInputMediaFormat == null){
_callBackError(KFByteBufferCodecErrorParams,"mInputMediaFormat null");
return;
}
///< 初始化 Codec 例項。
_setupCodec();
});
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void release() {
///< 釋放 Codec 例項、輸入快取。
mCodecHandler.post(()-> {
if(mMediaCodec != null){
try {
mMediaCodec.stop();
mMediaCodec.release();
} catch (Exception e) {
Log.e(TAG, "release: " + e.toString());
}
mMediaCodec = null;
}

mListLock.lock();
mList.clear();
mListCacheSize = 0;
mListLock.unlock();

mCodecThread.quit();
});
}

@Override
public MediaFormat getOutputMediaFormat() {
return mOutMediaFormat;
}

@Override
public MediaFormat getInputMediaFormat() {
return mInputMediaFormat;
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public int processFrame(KFFrame inputFrame) {
///< 處理輸入幀資料。
if(inputFrame == null){
return KFMediaCodeProcessParams;
}

KFBufferFrame frame = (KFBufferFrame)inputFrame;
if(frame.buffer ==null || frame.bufferInfo == null || frame.bufferInfo.size == 0){
return KFMediaCodeProcessParams;
}

///< 先新增到緩衝區,一旦緩衝區滿則返回 KFMediaCodeProcessAgainLater。
boolean appendSuccess = _appendFrame(frame);
if(!appendSuccess){
return KFMediaCodeProcessAgainLater;
}

mCodecHandler.post(()-> {
if(mMediaCodec == null){
return;
}

///< 子執行緒處理編解碼,從佇列取出一組資料,能塞多少就塞多少資料。
mListLock.lock();
int mListSize = mList.size();
mListLock.unlock();
while (mListSize > 0){
mListLock.lock();
KFBufferFrame packet = mList.get(0);
mListLock.unlock();

int bufferIndex;
try {
bufferIndex = mMediaCodec.dequeueInputBuffer(10 * 1000);
} catch (Exception e) {
Log.e(TAG, "dequeueInputBuffer" + e);
return;
}

if (bufferIndex >= 0) {
mInputBuffers[bufferIndex].clear();
mInputBuffers[bufferIndex].put(packet.buffer);
mInputBuffers[bufferIndex].flip();
try {
mMediaCodec.queueInputBuffer(bufferIndex, 0, packet.bufferInfo.size, packet.bufferInfo.presentationTimeUs, packet.bufferInfo.flags);
} catch (Exception e) {
Log.e(TAG, "queueInputBuffer" + e);
return;
}

mLastInputPts = packet.bufferInfo.presentationTimeUs;
mListLock.lock();
mList.remove(0);
mListSize = mList.size();
mListCacheSize -= packet.bufferInfo.size;
mListLock.unlock();
} else {
break;
}
}

///< 獲取 Codec 後的資料,一樣的策略,儘量拿出最多的資料出來,回撥給外層。
long outputDts = -1;
MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo();
while (outputDts < mLastInputPts) {
int bufferIndex;
try {
bufferIndex = mMediaCodec.dequeueOutputBuffer(outputBufferInfo, 10 * 1000);
} catch (Exception e) {
Log.e(TAG, "dequeueOutputBuffer" + e);
return;
}

if (bufferIndex >= 0) {
ByteBuffer decodeBuffer = mMediaCodec.getOutputBuffer(bufferIndex);
if (mListener != null) {
KFBufferFrame bufferFrame = new KFBufferFrame(decodeBuffer,outputBufferInfo);
mListener.dataOnAvailable(bufferFrame);
}
mMediaCodec.releaseOutputBuffer(bufferIndex,true);
} else {
if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mOutMediaFormat = mMediaCodec.getOutputFormat();
}
break;
}
}
});

return KFMediaCodeProcessSuccess;
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void flush() {
///< Codec 清空緩衝區,一般用於Seek、結束時時使用。
mCodecHandler.post(()-> {
if (mMediaCodec == null) {
return;
}

try {
mMediaCodec.flush();
} catch (Exception e) {
Log.e(TAG, "flush" + e);
}

mListLock.lock();
mList.clear();
mListCacheSize = 0;
mListLock.unlock();
});
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean _appendFrame(KFBufferFrame frame) {
///< 將輸入資料新增至緩衝區。
mListLock.lock();
int cacheSize = mListCacheSize;
mListLock.unlock();
if(cacheSize >= KFByteBufferCodecInputBufferMaxCache){
return false;
}

KFBufferFrame packet = new KFBufferFrame();

ByteBuffer newBuffer = ByteBuffer.allocateDirect(frame.bufferInfo.size);
newBuffer.put(frame.buffer).position(0);
MediaCodec.BufferInfo newInfo = new MediaCodec.BufferInfo();
newInfo.size = frame.bufferInfo.size;
newInfo.flags = frame.bufferInfo.flags;
newInfo.presentationTimeUs = frame.bufferInfo.presentationTimeUs;
packet.buffer = newBuffer;
packet.bufferInfo = newInfo;

mListLock.lock();
mList.add(packet);
mListCacheSize += packet.bufferInfo.size;
mListLock.unlock();

return true;
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private boolean _setupCodec() {
///< 初始化 Codec 模組,支援編碼、解碼,根據不同 MediaFormat 建立不同 Codec。
try {
String mimetype = mInputMediaFormat.getString(MediaFormat.KEY_MIME);
if (mIsEncoder) {
mMediaCodec = MediaCodec.createEncoderByType(mimetype);
} else {
mMediaCodec = MediaCodec.createDecoderByType(mimetype);
}
} catch (Exception e) {
Log.e(TAG, "createCodecByType" + e + mIsEncoder);
_callBackError(KFByteBufferCodecErrorCreate,e.getMessage());
return false;
}

try {
mMediaCodec.configure(mInputMediaFormat, null, null, mIsEncoder ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0);
} catch (Exception e) {
Log.e(TAG, "configure" + e);
_callBackError(KFByteBufferCodecErrorConfigure,e.getMessage());
return false;
}

try {
mMediaCodec.start();
mInputBuffers = mMediaCodec.getInputBuffers();
} catch (Exception e) {
Log.e(TAG, "start" + e );
_callBackError(KFByteBufferCodecErrorStart,e.getMessage());
return false;
}

return true;
}

private void _callBackError(int error, String errorMsg) {
if (mListener != null) {
mMainHandler.post(()->{
mListener.onError(error,TAG + errorMsg);
});
}
}
}

上面是 KFByteBufferCodec 的實現,從程式碼上可以看到主要有這幾個部分:

  • 1)建立與開啟編碼例項, _setupCodec ,呼叫  setup: 時才會建立編碼例項。
    • mIsEncoder
      createEncoderByType
      createDecoderByType
      configure
      MediaCodec.CONFIGURE_FLAG_ENCODE
      
    • start 在  _setupCodec 中執行,開啟音訊編碼。
  • 2)停止與清理編碼例項, release
    • stop 在  release 中執行,關閉音訊編碼。
  • 3)重新整理編碼緩衝區, flush ,通常編碼結束時將緩衝區資料刷新出來。
  • 4)處理音訊編碼資料, processFrame ,將編碼前資料放入緩衝區,編碼後資料拋給外層。
    • 輸入緩衝區佇列為  mList ,需要注意緩衝區有上限,一旦超過最大值則返回  KFMediaCodeProcessAgainLater ,防止因記憶體問題導致 OOM。
    • mList
      mList
      mList
      mLastInputPts
      mInputBuffers
      dequeueInputBuffer
      queueInputBuffer
      dequeueOutputBuffer
      getOutputBuffer
      releaseOutputBuffer
      
MediaCodec

我們又定義了類 KFAudioByteBufferEncoder ,繼承自  KFByteBufferCodec ,重寫了  processFrame release flush 三個方法。

KFAudioByteBufferEncoder.java

public class KFAudioByteBufferEncoder extends KFByteBufferCodec {
private int mChannel = 0; ///< 音訊聲道數
private int mSampleRate = 0; ///< 音訊取樣率
private long mCurrentTimestamp = -1; ///< 標記當前時間戳 (因為資料重新分割,所以時間戳需要手動計算)
private byte[] mByteArray = new byte[500 * 1024]; ///< 輸入音訊資料陣列
private int mByteArraySize = 0; ///< 輸入音訊資料 Size

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public int processFrame(KFFrame inputFrame) {
///< 獲取音訊聲道數與取樣率。
if (mChannel == 0) {
MediaFormat inputMediaFormat = getInputMediaFormat();
if (inputMediaFormat != null) {
mChannel = inputMediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mSampleRate = inputMediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
}
}

if (mChannel == 0 || mSampleRate == 0 || inputFrame == null) {
return KFMediaCodeProcessParams;
}

KFBufferFrame bufferFrame = (KFBufferFrame)inputFrame;
if (bufferFrame.bufferInfo == null || bufferFrame.bufferInfo.size == 0) {
return KFMediaCodeProcessParams;
}

///< 控制音訊輸入給編碼器單次位元組數 2048 位元組。
int sendSize = 2048;
///< 外層輸入如果為 2048 則直接跳過執行。
if (mByteArraySize == 0 && sendSize == bufferFrame.bufferInfo.size) {
return super.processFrame(inputFrame);
} else {
long currentTimestamp = 0;
if (mCurrentTimestamp == -1) {
mCurrentTimestamp = bufferFrame.bufferInfo.presentationTimeUs;
}

///< 將快取中資料執行送入編碼器操作。
int sendCacheStatus = sendBufferEncoder(sendSize);
if (sendCacheStatus < 0) {
return sendCacheStatus;
}

///< 將輸入資料送入緩衝區重複執行此操作。
byte[] inputBytes = new byte[bufferFrame.bufferInfo.size];
bufferFrame.buffer.get(inputBytes);

System.arraycopy(inputBytes,0,mByteArray,mByteArraySize,bufferFrame.bufferInfo.size);
mByteArraySize += bufferFrame.bufferInfo.size;

return sendBufferEncoder(sendSize);
}
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void release() {
mCurrentTimestamp = -1;
mByteArraySize = 0;
super.release();
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void flush() {
mCurrentTimestamp = -1;
mByteArraySize = 0;
super.flush();
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private int sendBufferEncoder(int sendSize) {
///< 將當前 Buffer 中資料按每次 2048 送給編碼器。
while (mByteArraySize >= sendSize) {
MediaCodec.BufferInfo newBufferInfo = new MediaCodec.BufferInfo();
newBufferInfo.size = sendSize;
newBufferInfo.presentationTimeUs = mCurrentTimestamp;

ByteBuffer newBuffer = ByteBuffer.allocateDirect(sendSize);
newBuffer.put(mByteArray,0,sendSize).position(0);

KFBufferFrame newFrame = new KFBufferFrame();
newFrame.buffer = newBuffer;
newFrame.bufferInfo = newBufferInfo;
int status = super.processFrame(newFrame);
if (status < 0) {
return status;
} else {
mByteArraySize -= sendSize;
if (mByteArraySize > 0) {
System.arraycopy(mByteArray, sendSize, mByteArray, 0, mByteArraySize);
}
}
mCurrentTimestamp += sendSize * 1000000 / (16 / 8 * mSampleRate * mChannel);
}
return KFMediaCodeProcessSuccess;
}
}

上面是 KFAudioByteBufferEncoder 的實現,主要就幹了一件事:拆分合適大小(2048 位元組)的資料送給編碼器。因為 AAC 資料編碼每 packet 大小為  1024 * 2(位深 16 Bit)

3、採集音訊資料進行 AAC 編碼和儲存

我們在一個 MainActivity 中來實現音訊採集及編碼邏輯,並將編碼後的資料加上  ADTS [1] 頭資訊儲存為 AAC 資料。

關於 ADTS,在 《音訊編碼:PCM 和 AAC 編碼》 中也有介紹,可以去看看了解一下。

MainActivity.java

public class MainActivity extends AppCompatActivity {
private FileOutputStream mStream = null;
private KFAudioCapture mAudioCapture = null; ///< 音訊採集模組
private KFAudioCaptureConfig mAudioCaptureConfig = null; ///< 音訊採集配置
private KFMediaCodecInterface mEncoder = null; ///< 音訊編碼
private MediaFormat mAudioEncoderFormat = null; ///< 音訊編碼格式描述
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) this,
new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}

mAudioCaptureConfig = new KFAudioCaptureConfig();
mAudioCapture = new KFAudioCapture(mAudioCaptureConfig,mAudioCaptureListener);
mAudioCapture.startRunning();

if (mStream == null) {
try {
mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.aac");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
startParams.gravity = Gravity.CENTER_HORIZONTAL;
Button startButton = new Button(this);
startButton.setTextColor(Color.BLUE);
startButton.setText("開始");
startButton.setVisibility(View.VISIBLE);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mEncoder == null) {
mEncoder = new KFAudioByteBufferEncoder();
MediaFormat mediaFormat = KFAVTools.createAudioFormat(mAudioCaptureConfig.sampleRate,mAudioCaptureConfig.channel,96*1000);
mEncoder.setup(true,mediaFormat,mAudioEncoderListener,null);
((Button)view).setText("停止");
} else {
mEncoder.release();
mEncoder = null;
((Button)view).setText("開始");
}
}
});
addContentView(startButton, startParams);
}

private KFAudioCaptureListener mAudioCaptureListener = new KFAudioCaptureListener() {
@Override
public void onError(int error, String errorMsg) {
Log.e("KFAudioCapture","errorCode" + error + "msg"+errorMsg);
}

@Override
public void onFrameAvailable(KFFrame frame) {
if (mEncoder != null) {
mEncoder.processFrame(frame);
}
}
};

private KFMediaCodecListener mAudioEncoderListener = new KFMediaCodecListener() {
@Override
public void onError(int error, String errorMsg) {
Log.i("KFMediaCodecListener","error" + error + "msg" + errorMsg);
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void dataOnAvailable(KFFrame frame) {
///< 音訊回撥資料
if (mAudioEncoderFormat == null && mEncoder != null) {
mAudioEncoderFormat = mEncoder.getOutputMediaFormat();
}
KFBufferFrame bufferFrame = (KFBufferFrame)frame;
try {
///< 新增ADTS資料
ByteBuffer adtsBuffer = KFAVTools.getADTS(bufferFrame.bufferInfo.size,mAudioEncoderFormat.getInteger(MediaFormat.KEY_PROFILE),mAudioEncoderFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),mAudioEncoderFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
byte[] adtsBytes = new byte[adtsBuffer.capacity()];
adtsBuffer.get(adtsBytes);
mStream.write(adtsBytes);

byte[] dst = new byte[bufferFrame.bufferInfo.size];
bufferFrame.buffer.get(dst);
mStream.write(dst);
} catch (IOException e) {
e.printStackTrace();
}
}
};
}

上面是 MainActivity  的實現,其中主要包含這幾個部分:

  • 1)在採集音訊前需要設定  Manifest.permission.RECORD_AUDIO 許可權。
  • 2)通過啟動和停止音訊採集來驅動整個採集和編碼流程。

  • 3)在採集模組  KFAudioCapture 的資料回撥中將資料交給編碼模組  KFAudioByteBufferEncoder 進行編碼。
    • 在  KFAudioCaptureListener 的  onFrameAvailable 回撥中實現。
  • 4)建立模組  KFAudioByteBufferEncoder  的  setup 中 MediaFormat。
    • 對應的實現在  KFAVTools 類的工具方法  static MediaFormat createVideoFormat(boolean isHEVC, Size size,int format,int bitrate,int fps,int gopDuration,int profile,int profileLevel) 中實現。
  • 5)在編碼模組  KFAudioByteBufferEncoder  的資料回撥中獲取編碼後的 AAC 裸流資料,並在每個 AAC packet 前寫入 ADTS 頭資料,儲存到檔案中。
    • 在  KFMediaCodecListener 的  dataOnAvailable 回撥中實現。
    • 其中生成一個 AAC packet 對應的 ADTS 頭資料在  KFAVTools 類的工具方法  static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel) 中實現。

KFAVTools.java

public class KFAVTools {

// 按音訊引數生產 AAC packet 對應的 ADTS 頭資料。
// 當編碼器編碼的是 AAC 裸流資料時,需要在每個 AAC packet 前新增一個 ADTS 頭用於解碼器解碼音訊流。
// 參考文件:
// ADTS 格式參考:http://wiki.multimedia.cx/index.php?title=ADTS
// MPEG-4 Audio 格式參考:http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations
public static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel) {
int sampleRateIndex = getSampleRateIndex(sampleRate);// 取得采樣率對應的 index。
int fullSize = 7 + size;
// ADTS 頭固定 7 位元組。
// 填充 ADTS 資料。
ByteBuffer adtsBuffer = ByteBuffer.allocateDirect(7);
adtsBuffer.order(ByteOrder.nativeOrder());
adtsBuffer.put((byte)0xFF); // 11111111 = syncword
adtsBuffer.put((byte)0xF1);
adtsBuffer.put((byte)(((profile - 1) << 6) + (sampleRateIndex << 2) + (channel >> 2)));
adtsBuffer.put((byte)(((channel & 3) << 6) + (fullSize >> 11)));
adtsBuffer.put((byte)((fullSize & 0x7FF) >> 3));
adtsBuffer.put((byte)(((fullSize & 7) << 5) + 0x1F));
adtsBuffer.put((byte)0xFC);
adtsBuffer.position(0);

return adtsBuffer;
}

private static int getSampleRateIndex(int sampleRate) {
int sampleRateIndex = 0;
switch (sampleRate) {
case 96000:
sampleRateIndex = 0;
break;
case 88200:
sampleRateIndex = 1;
break;
case 64000:
sampleRateIndex = 2;
break;
case 48000:
sampleRateIndex = 3;
break;
case 44100:
sampleRateIndex = 4;
break;
case 32000:
sampleRateIndex = 5;
break;
case 24000:
sampleRateIndex = 6;
break;
case 22050:
sampleRateIndex = 7;
break;
case 16000:
sampleRateIndex = 8;
break;
case 12000:
sampleRateIndex = 9;
break;
case 11025:
sampleRateIndex = 10;
break;
case 8000:
sampleRateIndex = 11;
break;
case 7350:
sampleRateIndex = 12;
break;
default:
sampleRateIndex = 15;
}
return sampleRateIndex;
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static MediaFormat createVideoFormat(boolean isHEVC, Size size,int format,int bitrate,int fps,int gopDuration,int profile,int profileLevel) {
String mimeType = isHEVC ? "video/hevc" : "video/avc";
MediaFormat mediaFormat = MediaFormat.createVideoFormat(mimeType, size.getWidth(), size.getHeight());

mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, format); //MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, gopDuration);
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, profile);
mediaFormat.setInteger(MediaFormat.KEY_LEVEL, profileLevel);

return mediaFormat;
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static MediaFormat createAudioFormat(int sampleRate, int channel, int bitrate) {
String mimeType = MediaFormat.MIMETYPE_AUDIO_AAC;
MediaFormat mediaFormat = MediaFormat.createAudioFormat(mimeType, sampleRate, channel);

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channel);
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);

return mediaFormat;
}
}

3、用工具播放 AAC 檔案

完成音訊採集和編碼後,可以將 sdcard 資料夾下面的  test.aac 檔案拷貝到電腦上,使用  ffplay 播放來驗證一下音訊採集是效果是否符合預期:

$ ffplay -i test.aac

這裡在播放 AAC 檔案時不必像播放 PCM 檔案那樣設定音訊引數,這正是因為我們已經將對應的引數資訊編碼到 ADTS 頭部資料中去了,播放解碼時可以從中解析出這些資訊從而正確的解碼 AAC。

關於播放 AAC 檔案的工具,可以參考 《FFmpeg 工具》第 2 節 ffplay 命令列工具《視覺化音視訊分析工具》第 1.1 節 Adobe Audition

參考資料

[1]

ADTS 格式: http://wiki.multimedia.cx/index.php?title=ADTS

- 完 -