Android AVDemo(6):音訊渲染,免費獲得原始碼丨音視訊工程示例

語言: CN / TW / HK

塞尚《自助餐》

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

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

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

這裡是 Android 第六篇: Android 音訊渲染 Demo 。這個 Demo 裡包含以下內容:

  • 1)實現一個音訊解封裝模組;

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

  • 3)實現一個音訊渲染模組;

  • 4)實現對 MP4 檔案中音訊部分的解封裝和解碼邏輯,並將解封裝、解碼後的資料送給渲染模組播放;

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

如果你想獲得全部原始碼和參與音視訊技術討論,可以通過下面二維碼加入『關鍵幀的音視訊開發圈』,當然也可以跳過直接看後續的內容。

長按識別二維碼→加入我們

1、音訊解封裝模組

在這個 Demo 中,解封裝模組 KFMP4Demuxer 的實現與 《Android 音訊解封裝 Demo》 中一樣,這裡就不再重複介紹了,其介面如下:

KFMP4Demuxer.java

public class KFMP4Demuxer {
public KFMP4Demuxer(KFDemuxerConfig config, KFDemuxerListener listener); ///< 構造方法 配置 & 回撥。
public void release(); ///< 釋放解封裝器例項。
public boolean hasVideo(); ///< 是否包含視訊。
public boolean hasAudio(); ///< 是否包含音訊。
public int duration(); ///< 檔案時長。
public int rotation(); ///< 視訊旋轉角度。
public boolean isHEVC(); ///< 是否為 H265。
public int width(); ///< 視訊寬度。
public int height(); ///< 視訊高度。
public int samplerate(); ///< 音訊取樣率。
public int channel(); ///< 音訊聲道數。
public int audioProfile(); ///< 音訊 profile。
public int videoProfile(); ///< 視訊 profile。
public MediaFormat audioMediaFormat(); ///< 音訊格式描述。
public MediaFormat videoMediaFormat(); ///< 視訊格式描述。
public ByteBuffer readAudioSampleData(MediaCodec.BufferInfo bufferInfo); ///< 讀取音訊幀。
public ByteBuffer readVideoSampleData(MediaCodec.BufferInfo bufferInfo); ///< 讀取視訊幀。
}

2、音訊解碼模組

同樣的,解碼模組 KFByteBufferCodec 的實現與 《Android 音訊解碼 Demo》 中一樣,這裡就不再重複介紹了,其介面如下:

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();
}

3、音訊渲染模組

接下來,我們來實現一個音訊渲染模組 KFAudioRender ,在這裡輸入解碼後的資料進行渲染播放。

KFAudioRenderListener.java

public interface KFAudioRenderListener {
///< 出錯回撥。
void onError(int error,String errorMsg);
///< 獲取PCM資料。
byte[] audioPCMData(int size);
}

上面是 KFAudioRenderListener 介面的設計,主要是有音訊渲染 資料輸入回撥錯誤回撥 的介面。

這裡重點需要看一下音訊渲染 資料輸入回撥 介面,系統的音訊渲染單元每次會主動通過回撥的方式要資料,我們這裡封裝的 KFAudioRender 則是用 資料輸入回撥 介面來從外部獲取一組待渲染的音訊資料送給系統的音訊渲染單元。

KFAudioRender.java

public class KFAudioRender {
private static final String TAG = "KFAudioRender";
public static final int KFAudioRenderErrorCreate = -2700;
public static final int KFAudioRenderErrorPlay = -2701;
public static final int KFAudioRenderErrorStop = -2702;
public static final int KFAudioRenderErrorPause = -2703;

private static final int KFAudioRenderMaxCacheSize = 500*1024; ///< 音訊 PCM 快取最大值。
private KFAudioRenderListener mListener = null; ///< 回撥。
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主執行緒。
private HandlerThread mThread = null; ///< 音訊管控執行緒。
private Handler mHandler = null;
private HandlerThread mRenderThread = null; ///< 音訊渲染執行緒。
private Handler mRenderHandler = null;
private AudioTrack mAudioTrack = null; ///< 音訊播放例項。
private int mMinBufferSize = 0;
private byte mCache[] = new byte[KFAudioRenderMaxCacheSize]; ///< 音訊 PCM 快取。
private int mCacheSize = 0;

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public KFAudioRender(KFAudioRenderListener listener, int sampleRate, int channel) {
mListener = listener;
///< 建立音訊管控執行緒。
mThread = new HandlerThread("KFAudioRenderThread");
mThread.start();
mHandler = new Handler((mThread.getLooper()));
///< 建立音訊渲染執行緒。
mRenderThread = new HandlerThread("KFAudioGetDataThread");
mRenderThread.start();
mRenderHandler = new Handler((mRenderThread.getLooper()));

mHandler.post(()->{
///< 初始化音訊播放例項。
_setupAudioTrack(sampleRate,channel);
});
}

public void release() {
mHandler.post(()-> {
///< 停止與釋放音訊播放例項。
if (mAudioTrack != null) {
try {
mAudioTrack.stop();
mAudioTrack.release();
} catch (Exception e) {
Log.e(TAG, "release: " + e.toString());
}
mAudioTrack = null;
}

mThread.quit();
mRenderThread.quit();
});
}

public void play() {
mHandler.post(()-> {
///< 音訊例項播放。
try {
mAudioTrack.play();
} catch (Exception e){
_callBackError(KFAudioRenderErrorPlay,e.getMessage());
return;
}

mRenderHandler.post(()->{
///< 迴圈寫入 PCM 資料,寫入系統緩衝區,當讀取到最大值或者狀態機不等於 STATE_INITIALIZED 則退出迴圈。
while (mAudioTrack.getState() == STATE_INITIALIZED){
if (mListener != null && mCacheSize < KFAudioRenderMaxCacheSize) {
byte[] bytes = mListener.audioPCMData(mMinBufferSize);
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes,0,mCache,mCacheSize,bytes.length);
mCacheSize += bytes.length;
if (mCacheSize >= mMinBufferSize) {
int writeSize = mAudioTrack.write(mCache,0,mMinBufferSize);
if (writeSize > 0) {
mCacheSize -= writeSize;
System.arraycopy(mCache,writeSize,mCache,0,mCacheSize);
}
}
} else {
break;
}
}
}
});
});
}

public void stop() {
///< 停止音訊播放。
mHandler.post(()-> {
try {
mAudioTrack.stop();
} catch (Exception e){
_callBackError(KFAudioRenderErrorStop,e.getMessage());
}
mCacheSize = 0;
});
}

public void pause() {
///< 暫停音訊播放。
mHandler.post(()-> {
try {
mAudioTrack.pause();
} catch (Exception e){
_callBackError(KFAudioRenderErrorPause,e.getMessage());
}
});
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void _setupAudioTrack(int sampleRate, int channel) {
///< 根據取樣率、聲道獲取每次音訊播放塞入資料大小,根據取樣率、聲道、資料大小建立音訊播放例項。
if (mAudioTrack == null) {
try {
mMinBufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, AudioFormat.ENCODING_PCM_16BIT);
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRate,channel == 2 ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO,AudioFormat.ENCODING_PCM_16BIT,mMinBufferSize,AudioTrack.MODE_STREAM);
} catch (Exception e){
_callBackError(KFAudioRenderErrorCreate,e.getMessage());
}
}
}

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

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

  • 1)建立音訊渲染例項。

    • _setupAudioTrack 方法中實現,根據取樣率、聲道、單次輸入資料大小 等幾個引數生成。
  • 2)處理音訊渲染例項的資料回撥,並在回撥中通過 KFAudioRender 的對外資料輸入回撥介面向更外層要待渲染的資料。
    • 通過 audioPCMData 回撥介面向更外層要資料。
  • 3)實現開始渲染和停止渲染邏輯。

    • play
      stop
      mHandler.post
      
    • 開啟播放後會迴圈向外層獲取 PCM 資料,通過 write 方法寫入 mAudioTrack
  • 4)清理音訊渲染例項。

    • release 方法中實現。

更具體細節見上述程式碼及其註釋。

4、解封裝和解碼 MP4 檔案中的音訊部分並渲染播放

我們在一個 MainActivity 中來實現從 MP4 檔案中解封裝和解碼音訊資料進行渲染播放。

MainActivity.java

public class MainActivity extends AppCompatActivity {
private KFDemuxer mDemuxer; ///< 音訊解封裝例項。
private KFDemuxerConfig mDemuxerConfig; ///< 音訊解決封裝配置。
private KFMediaCodecInterface mDecoder; ///< 音訊解碼例項。
private KFAudioRender mRender; ///< 音訊渲染例項。
private byte[] mPCMCache = new byte[10*1024*1024]; ///< PCM 資料快取。
private int mPCMCacheSize = 0;
private ReentrantLock mLock = new ReentrantLock(true);

@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);
}

///< 建立音訊解封裝配置。
mDemuxerConfig = new KFDemuxerConfig();
mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + "/test.aac";
mDemuxerConfig.demuxerType = KFGLBase.KFMediaType.KFMediaAudio;

///< 建立音訊解封裝例項。
mDemuxer = new KFDemuxer(mDemuxerConfig,mDemuxerListener);
mDecoder = new KFByteBufferCodec();
mDecoder.setup(false,mDemuxer.audioMediaFormat(),mDecoderListener,null);

///< 迴圈獲取解封裝資料塞入解碼器。
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
while (nextBuffer != null) {
mDecoder.processFrame(new KFBufferFrame(nextBuffer,bufferInfo));
nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
}

///< 建立音訊渲染例項。
mRender = new KFAudioRender(mRenderListener,mDemuxer.samplerate(),mDemuxer.channel());
mRender.play();
}

private KFDemuxerListener mDemuxerListener = new KFDemuxerListener() {
@Override
///< 解封裝出錯。
public void demuxerOnError(int error, String errorMsg) {
Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
}
};

private KFMediaCodecListener mDecoderListener = new KFMediaCodecListener() {
@Override
///< 解碼出錯。
public void onError(int error, String errorMsg) {

}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
///< 解碼資料回撥儲存到本地 PCM 快取,Demo 處理比較簡單,沒有考慮到渲染暫停解碼不暫停等 case,可能存在緩衝區溢位。
public void dataOnAvailable(KFFrame frame) {
KFBufferFrame bufferFrame = (KFBufferFrame)frame;
if (bufferFrame.buffer != null && bufferFrame.bufferInfo.size > 0) {
byte[] bytes = new byte[bufferFrame.bufferInfo.size];
bufferFrame.buffer.get(bytes);
mLock.lock();
System.arraycopy(bytes,0,mPCMCache,mPCMCacheSize,bytes.length);
mPCMCacheSize += bytes.length;
mLock.unlock();
}
}
};

private KFAudioRenderListener mRenderListener = new KFAudioRenderListener() {
@Override
///< 音訊渲染出錯。
public void onError(int error, String errorMsg) {

}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
///< 音訊播放模組獲取音訊 PCM 資料。
public byte[] audioPCMData(int size) {
if (mPCMCacheSize >= size) {
byte[] dst = new byte[size];
mLock.lock();
System.arraycopy(mPCMCache,0,dst,0,size);
mPCMCacheSize -= size;
System.arraycopy(mPCMCache,size,mPCMCache,0,mPCMCacheSize);
mLock.unlock();
return dst;
}
return null;
}
};
}

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

  • 1)在頁面載入完成後就啟動解封裝和解碼模組,並且迴圈讀取音訊資料傳遞給解碼器。

    • onCreate 中實現。
  • 2)在解碼模組 KFByteBufferCodec 的資料回撥中獲取解碼後的 PCM 資料緩衝起來等待渲染。
    • KFMediaCodecListenerdataOnAvailable 回撥中實現。
  • 3)在渲染模組 KFAudioRender 的輸入資料回撥中把緩衝區的資料交給系統音訊渲染單元渲染。
    • KFAudioRenderListeneraudioPCMData 回撥中實現。

更具體細節見上述程式碼及其註釋。

- 完 -

推薦閱讀
《Android AVDemo(5):音訊解碼》
《Android AVDemo(4):音訊解封裝》
《Android AVDemo(3):音訊封裝》
《Android AVDemo(2):音訊編碼》
《Android AVDemo(1):音訊採集》
《iOS AVDemo(6):音訊渲染》
《iOS AVDemo(5):音訊解碼》
《iOS AVDemo(4):音訊解封裝》
《iOS AVDemo(3):音訊封裝》
《iOS AVDemo(2):音訊編碼》
《iOS AVDemo(1):音訊採集》
加我微信,拉你入群

謝謝看完全文,也點一下『贊』和 『在看』吧 ↓