Android AVDemo(1):音訊採集,免費獲取全部原始碼丨音影片工程示例

語言: CN / TW / HK

塞尚《聖維克多山》

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

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

- 完 -

推薦閱讀

《iOS AVDemo(1):音訊採集》
《FFmpeg 工具:音影片開發都用它,快@你兄弟來看》
《視覺化音影片分析工具:好用工具大集錦,快轉發給你兄弟看看》
《資料抓包工具:看看競品的協議都做了哪些優化》
加我微信,拉你入群

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