Android音訊開發之使用AudioRecord錄製
本文主要是記錄Android端音訊開發
本例記錄使用AudioRecord 錄製音訊,播放使用AudioTrack,儲存的檔案為pcm
只是簡單的測試用例,介面同上文
注意新增許可權
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
1 . AudioRecord 的工作流程:--- 配置引數,初始化內部的音訊緩衝區
--- 開始採集
--- 需要一個執行緒,不斷地從 AudioRecord 的緩衝區將音訊資料“讀”出來,注意,這個過程一定要及時,否則就會出現“overrun”的錯誤,該錯誤在音訊開發中比較常見,意味著應用層沒有及時地“取走”音訊資料,導致內部的音訊緩衝區溢位。
--- 停止採集,釋放資源
引數配置
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes) throws IllegalArgumentException
--- audioSource該引數指的是音訊採集的輸入源,可選的值以常量的形式定義在 MediaRecorder.AudioSource 類中,常用的值包括:DEFAULT(預設),VOICE_RECOGNITION(用於語音識別,等同於DEFAULT),MIC(由手機麥克風輸入),VOICE_COMMUNICATION(用於VoIP應用)等等。
--- sampleRateInHz
取樣率,注意,目前44100Hz是唯一可以保證相容所有Android手機的取樣率。
--- channelConfig
通道數的配置,可選的值以常量的形式定義在 AudioFormat 類中,常用的是 CHANNEL_IN_MONO(單通道),CHANNEL_IN_STEREO(雙通道)
--- audioFormat
這個引數是用來配置“資料位寬”的,可選的值也是以常量的形式定義在 AudioFormat 類中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保證相容所有Android手機的。
--- bufferSizeInBytes
這個是最難理解又最重要的一個引數,它配置的是 AudioRecord 內部的音訊緩衝區的大小,該緩衝區的值不能低於一幀“音訊幀”(Frame)的大小,一幀音訊幀的大小計算如下:
int size = 取樣率 x 位寬 x 取樣時間 x 通道數
取樣時間一般取 2.5ms~120ms 之間,由廠商或者具體的應用決定,我們其實可以推斷,每一幀的取樣時間取得越短,產生的延時就應該會越小,當然,碎片化的資料也就會越多。
在Android開發中,AudioRecord 類提供了一個幫助你確定這個 bufferSizeInBytes 的函式,原型如下:
int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);
強烈建議由該函式計算出需要傳入的 bufferSizeInBytes,而不是自己手動計算。
當建立好了 AudioRecord 物件之後,就可以開始進行音訊資料的採集了,通過下面兩個函式控制採集的開始/停止:
AudioRecord.startRecording();
AudioRecord.stop();
呼叫的讀取資料的介面是:
AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);
2 . AudioTrack 的工作流程:
--- 配置引數,初始化內部的音訊播放緩衝區
--- 開始播放
--- 需要一個執行緒,不斷地向 AudioTrack 的緩衝區“寫入”音訊資料
--- 停止播放,釋放資源
引數設定
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) throws IllegalArgumentException
--- streamType這個引數代表著當前應用使用的哪一種音訊管理策略,當系統有多個程序需要播放音訊時,這個管理策略會決定最終的展現效果,該引數的可選的值以常量的形式定義在 AudioManager 類中,主要包括:
STREAM_VOCIE_CALL:電話聲音
STREAM_SYSTEM:系統聲音
STREAM_RING:鈴聲
STREAM_MUSCI:音樂聲
STREAM_ALARM:警告聲
STREAM_NOTIFICATION:通知聲
--- sampleRateInHz
取樣率,從AudioTrack原始碼的“audioParamCheck”函式可以看到,這個取樣率的取值範圍必須在 4000Hz~192000Hz 之間。
--- channelConfig
通道數的配置,可選的值以常量的形式定義在 AudioFormat 類中,常用的是 CHANNEL_IN_MONO(單通道),CHANNEL_IN_STEREO(雙通道)
--- audioFormat
這個引數是用來配置“資料位寬”的,可選的值也是以常量的形式定義在 AudioFormat 類中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保證相容所有Android手機的。
--- bufferSizeInBytes
配置的是 AudioTrack 內部的音訊緩衝區的大小,該緩衝區的值不能低於一幀“音訊幀”(Frame)的大小,一幀音訊幀的大小計算如下:
int size = 取樣率 x 位寬 x 取樣時間 x 通道數
AudioTrack 類提供了一個幫助你確定這個 bufferSizeInBytes 的函式,原型如下:
int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);
--- mode
AudioTrack 提供了兩種播放模式,一種是 static 方式,一種是 streaming 方式,前者需要一次性將所有的資料都寫入播放緩衝區,簡單高效,通常用於播放鈴聲、系統提醒的音訊片段; 後者則是按照一定的時間間隔不間斷地寫入音訊資料,理論上它可用於任何音訊播放的場景。
在 AudioTrack 類中,一個是 MODE_STATIC,另一個是 MODE_STREAM
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.cl.slack.mediarecorder.R;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
public class AudioRecorderActivity extends AppCompatActivity
implements View.OnClickListener {
private final int REQ_PERMISSION_AUDIO = 0x01;
private Button mRecode, mPlay;
private File mAudioFile = null;
private Thread mCaptureThread = null;
private boolean mIsRecording,mIsPlaying;
private int mFrequence = 44100;
private int mChannelConfig = AudioFormat.CHANNEL_IN_MONO;
private int mPlayChannelConfig = AudioFormat.CHANNEL_IN_STEREO;
private int mAudioEncoding = AudioFormat.ENCODING_PCM_16BIT;
private PlayTask mPlayer;
private RecordTask mRecorder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_recorder_audio);
mRecode = (Button) findViewById(R.id.audio_recode);
mPlay = (Button) findViewById(R.id.audio_paly);
mRecode.setText("recode");
mPlay.setText("play");
mPlay.setEnabled(false);
mRecode.setOnClickListener(this);
mPlay.setOnClickListener(this);
// mRecorder = new RecordTask();
// mPlayer = new PlayTask();
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.audio_recode:
if (mRecode.getTag() == null) {
startAudioRecode();
} else {
stopAudioRecode();
}
break;
case R.id.audio_paly:
if (mPlay.getTag() == null) {
startAudioPlay();
} else {
stopAudioPlay();
}
break;
}
}
private void startAudioRecode() {
if (checkPermission()) {
PackageManager packageManager = this.getPackageManager();
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) {
showToast("This device doesn't have a mic!");
return;
}
mRecode.setTag(this);
mRecode.setText("stop");
mPlay.setEnabled(false);
File fpath = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/slack");
fpath.mkdirs();// 建立資料夾
try {
// 建立臨時檔案,注意這裡的格式為.pcm
mAudioFile = File.createTempFile("recording", ".pcm", fpath);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mRecorder = new RecordTask();
mRecorder.execute();
showToast("Recording started");
} else {
requestPermission();
}
}
private void stopAudioRecode() {
mIsRecording = false;
mRecode.setTag(null);
mRecode.setText("recode");
mPlay.setEnabled(true);
showToast("Recording Completed");
}
private void startAudioPlay() {
mPlay.setTag(this);
mPlay.setText("stop");
mPlayer = new PlayTask();
mPlayer.execute();
showToast("Recording Playing");
}
private void stopAudioPlay() {
mIsPlaying = false;
mPlay.setTag(null);
mPlay.setText("play");
}
private boolean checkPermission() {
int result = ContextCompat.checkSelfPermission(getApplicationContext(),
WRITE_EXTERNAL_STORAGE);
int result1 = ContextCompat.checkSelfPermission(getApplicationContext(),
RECORD_AUDIO);
return result == PackageManager.PERMISSION_GRANTED &&
result1 == PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this, new
String[]{WRITE_EXTERNAL_STORAGE, RECORD_AUDIO}, REQ_PERMISSION_AUDIO);
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case REQ_PERMISSION_AUDIO:
if (grantResults.length > 0) {
boolean StoragePermission = grantResults[0] ==
PackageManager.PERMISSION_GRANTED;
boolean RecordPermission = grantResults[1] ==
PackageManager.PERMISSION_GRANTED;
if (StoragePermission && RecordPermission) {
showToast("Permission Granted");
} else {
showToast("Permission Denied");
}
}
break;
}
}
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
class RecordTask extends AsyncTask<Void,Integer,Void> {
@Override
protected Void doInBackground(Void... arg0) {
mIsRecording = true;
try {
// 開通輸出流到指定的檔案
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(mAudioFile)));
// 根據定義好的幾個配置,來獲取合適的緩衝大小
int bufferSize = AudioRecord.getMinBufferSize(mFrequence,
mChannelConfig, mAudioEncoding);
// 例項化AudioRecord
AudioRecord record = new AudioRecord(
MediaRecorder.AudioSource.MIC, mFrequence,
mChannelConfig, mAudioEncoding, bufferSize);
// 定義緩衝
short[] buffer = new short[bufferSize];
// 開始錄製
record.startRecording();
int r = 0; // 儲存錄製進度
// 定義迴圈,根據isRecording的值來判斷是否繼續錄製
while (mIsRecording) {
// 從bufferSize中讀取位元組,返回讀取的short個數
int bufferReadResult = record
.read(buffer, 0, buffer.length);
// 迴圈將buffer中的音訊資料寫入到OutputStream中
for (int i = 0; i < bufferReadResult; i++) {
dos.writeShort(buffer[i]);
}
publishProgress(new Integer(r)); // 向UI執行緒報告當前進度
r++; // 自增進度值
}
// 錄製結束
record.stop();
Log.i("slack", "::" + mAudioFile.length());
dos.close();
} catch (Exception e) {
// TODO: handle exception
Log.e("slack", "::" + e.getMessage());
}
return null;
}
// 當在上面方法中呼叫publishProgress時,該方法觸發,該方法在UI執行緒中被執行
protected void onProgressUpdate(Integer... progress) {
//
}
protected void onPostExecute(Void result) {
}
}
/**
* AudioTrack
*/
class PlayTask extends AsyncTask<Void,Void,Void> {
@Override
protected Void doInBackground(Void... arg0) {
mIsPlaying = true;
int bufferSize = AudioTrack.getMinBufferSize(mFrequence,
mPlayChannelConfig, mAudioEncoding);
short[] buffer = new short[bufferSize ];
try {
// 定義輸入流,將音訊寫入到AudioTrack類中,實現播放
DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(mAudioFile)));
// 例項AudioTrack
AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC,
mFrequence,
mPlayChannelConfig, mAudioEncoding, bufferSize,
AudioTrack.MODE_STREAM);
// 開始播放
track.play();
// 由於AudioTrack播放的是流,所以,我們需要一邊播放一邊讀取
while (mIsPlaying && dis.available() > 0) {
int i = 0;
while (dis.available() > 0 && i < buffer.length) {
buffer[i] = dis.readShort();
i++;
}
// 然後將資料寫入到AudioTrack中
track.write(buffer, 0, buffer.length);
}
// 播放結束
track.stop();
dis.close();
} catch (Exception e) {
// TODO: handle exception
Log.e("slack","error:" + e.getMessage());
}
return null;
}
protected void onPostExecute(Void result) {
}
protected void onPreExecute() {
}
}
}