Android AVDemo(1):音訊採集,免費獲取全部原始碼丨音影片工程示例
塞尚《聖維克多山》
這個公眾號會 路線圖 式的遍歷分享音影片技術 : 音影片基礎(完成) → 音影片工具(完成) → 音影片工程示例(進行中) → 音影片工業實戰(準備) 。 關注一下成本不高,錯過乾貨損失不小 ↓↓↓
iOS/Android 客戶端開發同學如果想要開始學習音影片開發,最絲滑的方式是對音影片基礎概念知識有一定了解後,再借助 iOS/Android 平臺的音影片能力上手去實踐音影片的 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
過程,並藉助音影片工具來分析和理解對應的音影片資料。
在音影片工程示例這個欄目,我們將通過拆解 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
流程並實現 Demo 來向大家介紹如何在 iOS/Android 平臺上手音影片開發。
這裡是 Android 第一篇: Android 音訊採集 Demo 。這個 Demo 裡包含以下內容:
-
1)實現一個音訊採集模組;
-
2)實現音訊採集邏輯並將採集的音訊儲存為 PCM 資料;
-
3)詳盡的程式碼註釋,幫你理解程式碼邏輯和原理。
在本文中,我們將詳解一下 Demo 的具體實現和原始碼。讀完本文內容相信就能幫你掌握相關知識。
1、音訊採集模組
首先,實現一個 KFAudioConfig
類用於定義音訊採集引數的配置。這裡包括了:取樣率、聲道數這幾個引數。這幾個引數的含義在前面介紹聲音基礎的文章 聲音的表示(3):聲音的數字化
中有過介紹。
KFAudioCaptureConfig.java
public class KFAudioCaptureConfig {
public int sampleRate = 44100;
public int channel = 1;
}
接下來,我們實現一個 KFAudioCaptureListener
類來實現採集回撥,包含錯誤回撥與資料回撥。
KFAudioCaptureListener.java
public interface KFAudioCaptureListener {
void onError(int error,String errorMsg);
void onFrameAvailable(KFFrame frame);
}
上面的 KFFrame
是音訊資料物件,資料包含 Buffer 資料與 Texture 資料,音訊僅涉及 Buffer 資料。
KFFrame.java
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class KFFrame {
public enum KFFrameType {
KFFrameBuffer,
KFFrameTexture;
}
public KFFrameType frameType = KFFrameType.KFFrameBuffer;
public KFFrame(KFFrameType type) {
frameType = type;
}
}
音訊 Buffer 資料 KFBufferFrame
,繼承自 KFFrame
,包含 ByteBuffer 資料與 BufferInfo 資料資訊。BufferInfo 為了提供時間戳 presentationTimeUs 與 size。
KFBufferFrame.java
public class KFBufferFrame extends KFFrame {
public ByteBuffer buffer;
public MediaCodec.BufferInfo bufferInfo;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public KFBufferFrame() {
super(KFFrameBuffer);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public KFBufferFrame(ByteBuffer inputBuffer, MediaCodec.BufferInfo inputBufferInfo) {
super(KFFrameBuffer);
buffer = inputBuffer;
bufferInfo = inputBufferInfo;
}
public KFFrameType frameType() {
return KFFrameBuffer;
}
}
最後我們實現一個 KFAudioCapture
類來實現音訊採集。
KFAudioCapture.java
public class KFAudioCapture {
public static int KFAudioCaptureErrorCreate = -2600;
public static int KFAudioCaptureErrorStart = -2601;
public static int KFAudioCaptureErrorStop = -2602;
private static final String TAG = "KFAudioCapture";
private KFAudioCaptureConfig mConfig = null; ///< 音訊配置
private KFAudioCaptureListener mListener = null; ///< 音訊回撥
private HandlerThread mRecordThread = null; ///< 音訊採集執行緒
private Handler mRecordHandle = null;
private HandlerThread mReadThread = null; ///< 音訊讀資料執行緒
private Handler mReadHandle = null;
private int mMinBufferSize = 0;
private AudioRecord mAudioRecord = null; ///< 音訊採集例項
private boolean mRecording = false;
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主執行緒用作錯誤回撥
public KFAudioCapture(KFAudioCaptureConfig config,KFAudioCaptureListener listener) {
mConfig = config;
mListener = listener;
mRecordThread = new HandlerThread("KFAudioCaptureThread");
mRecordThread.start();
mRecordHandle = new Handler((mRecordThread.getLooper()));
mReadThread = new HandlerThread("KFAudioCaptureReadThread");
mReadThread.start();
mReadHandle = new Handler((mReadThread.getLooper()));
mRecordHandle.post(()->{
///< 初始化音訊採集例項。
_setupAudioRecord();
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startRunning() {
///< 開啟音訊採集。
mRecordHandle.post(()->{
if (mAudioRecord != null && !mRecording) {
try {
mAudioRecord.startRecording();
mRecording = true;
} catch (Exception e) {
Log.e(TAG,e.getMessage());
_callBackError(KFAudioCaptureErrorStart,e.getMessage());
}
///< 音訊採集採用拉資料模式,通過讀資料執行緒開啟迴圈無限拉取 PCM 資料,拉到資料後進行回撥。
mReadHandle.post(()->{
while (mRecording) {
final byte[] pcmData = new byte[mMinBufferSize];
int readSize = mAudioRecord.read(pcmData, 0, mMinBufferSize);
if (readSize > 0) {
///< 處理音訊資料 data。
ByteBuffer buffer = ByteBuffer.allocateDirect(readSize).put(pcmData).order(ByteOrder.nativeOrder());
buffer.position(0);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
bufferInfo.presentationTimeUs = System.nanoTime() / 1000;
bufferInfo.size = readSize;
KFBufferFrame bufferFrame = new KFBufferFrame(buffer,bufferInfo);
if (mListener != null) {
mListener.onFrameAvailable(bufferFrame);
}
}
}
});
}
});
}
public void stopRunning() {
///< 關閉音訊採集。
mRecordHandle.post(()->{
if (mAudioRecord != null && mRecording) {
try {
mAudioRecord.stop();
mRecording = false;
} catch (Exception e) {
Log.e(TAG,e.getMessage());
_callBackError(KFAudioCaptureErrorStart,e.getMessage());
}
}
});
}
public void release() {
///< 外層主動觸發釋放,釋放採集例項、執行緒。
mRecordHandle.post(()->{
if (mAudioRecord != null) {
if (mRecording) {
try {
mAudioRecord.stop();
mRecording = false;
} catch (Exception e) {
Log.e(TAG,e.getMessage());
}
}
try {
mAudioRecord.release();
} catch (Exception e) {
Log.e(TAG,e.getMessage());
}
mAudioRecord = null;
}
mRecordThread.quit();
mReadThread.quit();
});
}
private void _setupAudioRecord() {
if (mAudioRecord == null) {
///< 根據指定取樣率、聲道、位深獲取每次回撥資料大小。
mMinBufferSize = AudioRecord.getMinBufferSize(mConfig.sampleRate, mConfig.channel, AudioFormat.ENCODING_PCM_16BIT);
try {
///< 根據取樣率、聲道、位深每次回撥資料大小生成採集例項。
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,mConfig.sampleRate,mConfig.channel, AudioFormat.ENCODING_PCM_16BIT,mMinBufferSize);
} catch (Exception e) {
Log.e(TAG,e.getMessage());
_callBackError(KFAudioCaptureErrorCreate,e.getMessage());
};
}
}
private void _callBackError(int error, String errorMsg) {
///< 錯誤回撥。
if (mListener != null) {
mMainHandler.post(()->{
mListenjavaer.onError(error,TAG + errorMsg);
});
}
}
}
上面是 KFAudioCapture
的實現,從程式碼上可以看到主要有這幾個部分:
-
1)建立音訊採集例項,
_setupAudioRecord
根據取樣率、聲道、位深、回撥資料大小來建立音訊採集例項。每次回撥資料大小這裡反應拉取資料的頻率,對於直播等場景可以設定小一些,有利於降低延遲。 -
2)開啟音訊採集,
startRunning
,這裡需要關注開啟單獨執行緒拉取 PCM 資料任務,將拉取到的資料回撥給外層。 -
3)關閉音訊採集,
stopRunning
。 -
4)清理音訊採集例項,
release
。
2、採集音訊儲存為 PCM 檔案
我們在一個 MainActivity
中來實現音訊採集邏輯並將採集的音訊儲存為 PCM 資料。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private FileOutputStream mStream = null;
private KFAudioCapture mAudioCapture = null;
private KFAudioCaptureConfig mAudioCaptureConfig = 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.requestPermissions((Activity) this,
new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO},
1);
}
mAudioCaptureConfig = new KFAudioCaptureConfig();
mAudioCapture = new KFAudioCapture(mAudioCaptureConfig,mAudioCaptureListener);
mAudioCapture.startRunning();
if (mStream == null) {
try {
mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.pcm");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
///< 音訊採集回撥。
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) {
///< 獲取到音訊 Buffer 資料儲存到本地 PCM。
try {
ByteBuffer pcmData = ((KFBufferFrame)frame).buffer;
byte[] ppsBytes = new byte[pcmData.capacity()];
pcmData.get(ppsBytes);
mStream.write(ppsBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
};
}
上面是 MainActivity
的實現,這裡需要注意的是在採集音訊前需要判斷錄製許可權 Manifest.permission.RECORD_AUDIO
。
3、用工具播放 PCM 檔案
完成音訊採集後,可以將 sdcard
資料夾下面的 test.pcm
檔案拷貝到電腦上,使用 ffplay
播放來驗證一下音訊採集是效果是否符合預期:
$ ffplay -ar 44100 -channels 1 -f s16le -i test.pcm
注意這裡的引數要對齊在工程程式碼中設定的 取樣率
、 聲道數
、 取樣位深
。
關於播放 PCM 檔案的工具,可以參考 《FFmpeg 工具》第 2 節 ffplay 命令列工具 和 《視覺化音影片分析工具》第 1.1 節 Adobe Audition 。
- 完 -
推薦閱讀
謝謝看完全文,也點一下『贊』和 『在看』吧 ↓
- 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 都支援丨音影片工程示例