Android AVDemo(6):音訊渲染,免費獲得原始碼丨音影片工程示例
塞尚《自助餐》
這個公眾號會 路線圖 式的遍歷分享音影片技術 : 音影片基礎(完成) → 音影片工具(完成) → 音影片工程示例(進行中) → 音影片工業實戰(準備) 。 關注一下成本不高,錯過乾貨損失不小 ↓↓↓
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 資料緩衝起來等待渲染。 -
在
KFMediaCodecListener
的dataOnAvailable
回撥中實現。 -
3)在渲染模組
KFAudioRender
的輸入資料回撥中把緩衝區的資料交給系統音訊渲染單元渲染。 -
在
KFAudioRenderListener
的audioPCMData
回撥中實現。
更具體細節見上述程式碼及其註釋。
- 完 -
謝謝看完全文,也點一下『贊』和 『在看』吧 ↓
- WWDC 2022 音影片相關 Session 概覽(EDR 相關)丨音影片工程示例
- 音影片知識圖譜 2022.06
- Android AVDemo(13):影片渲染丨音影片工程示例
- 想在自己的影片平臺支援 HDR 需要做哪些工作?丨有問有答
- Android AVDemo(11):影片轉封裝,從 MP4 到 MP4丨音影片工程示例
- 音影片面試題集錦 2022.05
- Android AVDemo(6):音訊渲染,免費獲得原始碼丨音影片工程示例
- Android AVDemo(4):音訊解封裝,從 MP4 中解封裝出 AAC丨音影片工程示例
- 如何根據 NALU 裸流資料來判斷其是 H.264 還是 H.265 編碼?丨有問有答
- 音影片知識圖譜 2022.04
- Android AVDemo(2):音訊編碼,採集 PCM 資料編碼為 AAC丨音影片工程示例
- 音影片面試題集錦 2022.04
- Android AVDemo(1):音訊採集,免費獲取全部原始碼丨音影片工程示例
- iOS 影片處理框架及重點 API 合集丨音影片工程示例
- iOS AVDemo(13):影片渲染,用 Metal 渲染丨音影片工程示例
- 如何像抖音直播一樣,從 App 直播間到桌面畫中畫實現畫面無縫切換?丨有問有答
- 如何在影片採集流水線中增加濾鏡處理節點?丨有問有答
- iOS AVDemo(11):影片轉封裝,從 MP4 到 MP4丨音影片工程示例
- 音影片知識圖譜 2022.03
- iOS AVDemo(8):影片編碼,H.264 和 H.265 都支援丨音影片工程示例