Android 音頻錄製(二)-AudioRecord
Android 系統爲我們提供了三種錄製音頻的方式
- MediaRecord( Java API)
- AudioRecord( Java API)
- OpenSL ES( Native API)
這次我們來說複雜一點的 AudioRecord。官方API 介紹
我們在上節說的 使用MeidaRecord錄製,系統已經爲我們將數據處理完成,我們只需要傳入一個目標文件路徑,音頻文件的錄製,保存就全部完成了。
而 AudioRecord 則是可以影響到我們在 音頻基礎知識 中說的 模擬信號到數字信號 的轉換流程。
初始化
先來看一下它的構造函數
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes){
}
//或者
AudioRecord recorder = new AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(32000)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build())
.setBufferSizeInBytes(2*minBuffSize)
.build();
其中四個參數分別代表,這四個參數對於信號的處理我們在 音頻基礎知識 中已經說到,這裏就不在多說
- audioSource:錄製音頻來源
- sampleRateInHz:採樣率,單位Hz(赫茲)
- channelConfig:聲道數
- audioFormat:返回的音頻數據的編碼格式
錄製
AudioRecord 創建完成後。需要注意的是,錄製需要在子線程中,不能再主線程中進行錄製。
/**
* Author silence.
* Time:2019-09-20.
* Desc:pcm 錄製,使用 Android 原生的 AudioRecord 進行錄製
*/
final class PcmRecord implements Runnable {
private static final String TAG = "PcmRecord";
//錄製的音頻來源
private static final int RECORD_SOURCE = MediaRecorder.AudioSource.MIC;
//採樣率,單位Hz(赫茲)
private static final int SAMPLE_RATE_IN_HZ = 16000;
//音頻聲道的配置(輸入) AudioFormat.CHANNEL_IN_MONO 單聲道
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
//返回的音頻數據的編碼格式,每份採樣數據爲PCM 16bit,保證所有設備支持
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
//在錄音時期,音頻數據寫入的緩衝區的整體大小(單位字節),即緩衝區的大小
private static int BUFFER_SIZE_IN_BYTES = 1280;
private static final Object mLock = new Object();
@NonNull
private AudioRecord mAudioRecord;
private boolean isRecording = false;
private Thread mRecordThread;
PcmRecord() {
try {
mAudioRecord = new AudioRecord(RECORD_SOURCE, SAMPLE_RATE_IN_HZ,
CHANNEL_CONFIG, AUDIO_FORMAT, BUFFER_SIZE_IN_BYTES);
} catch (Throwable t) {
BUFFER_SIZE_IN_BYTES = AudioRecord.getMinBufferSize(SAMPLE_RATE_IN_HZ, CHANNEL_CONFIG, AUDIO_FORMAT);
SLog.e(TAG, "audio record init error, use minBufferSize = " + BUFFER_SIZE_IN_BYTES, t);
mAudioRecord = new AudioRecord(RECORD_SOURCE, SAMPLE_RATE_IN_HZ,
CHANNEL_CONFIG, AUDIO_FORMAT, BUFFER_SIZE_IN_BYTES);
}
}
void start() {
synchronized (mLock) {
if (isRecording) {
SLog.d(TAG, "正在錄音,重複調用 start()");
return;
}
try {
mAudioRecord.startRecording();
isRecording = true;
startReadRecord();
} catch (Throwable t) {
SLog.e(TAG, "開始錄音失敗", t);
}
}
}
@Override
public void run() {
SLog.d(TAG, "讀取錄音數據-開始");
mLastRecordTimeMillis = System.currentTimeMillis();
dispatchRecordResult(SpeechUtility.START, null);
try {
byte[] recordBytes = new byte[BUFFER_SIZE_IN_BYTES];
while (isRecording) {
int size = mAudioRecord.read(recordBytes, 0, BUFFER_SIZE_IN_BYTES);
dispatchRecordResult(SpeechUtility.RECOGNIZING, recordBytes);
}
SLog.d(TAG, "讀取錄音數據-結束");
} catch (Throwable t) {
SLog.e(TAG, "讀取錄音數據-異常", t);
dispatchRecordResult(SpeechUtility.ERROR, null);
stop();
}
dispatchRecordResult(SpeechUtility.END, null);
}
private void dispatchRecordResult(int state, byte[] data) {
RecordResult recordResult = new RecordResult();
recordResult.recordData = data;
recordResult.status = state;
Pcm2File.write(recordResult);
}
/**
* 開始讀取錄音的數據
*/
private void startReadRecord() {
cancelReadRecordThread();
mRecordThread = new Thread(this);
mRecordThread.start();
}
/**
* 是否正在錄音
*/
public boolean isRecording() {
return isRecording;
}
private void cancelReadRecordThread() {
try {
if (mRecordThread != null && !mRecordThread.isInterrupted()) {
mRecordThread.interrupt();
}
} catch (Throwable t) {
}
}
void stop() {
synchronized (mLock) {
if (!isRecording) {
SLog.d(TAG, "已經停止錄音,重複調用 stop()");
return;
}
isRecording = false;
try {
mAudioRecord.stop();
} catch (Throwable t) {
SLog.e(TAG, "停止錄音異常", t);
}
mRecordThread = null;
}
}
}
這裏的 RecordResult 是對錄音數據的一個封裝
/**
* Author silence.
* Time:2019-09-23.
* Desc:錄音數據計算音量大小
*/
public class RecordResult extends SpeechResult{
public int state;
public byte[] recordData = null;
}
其中的 Pcm2File 負責將錄製的 PCM 數據寫入文件中,並在錄製完成後,將 PCM 源數據轉換成可以直接播放的 wav 音頻文件。
/**
* Author silence.
* Time:2019-09-25.
* Desc:todo release 不需要
*/
public class Pcm2File {
private static String path = SpeechSystem.application.getExternalFilesDir("pcm") + "/record.pcm";
private static String wavPath = SpeechSystem.application.getExternalFilesDir("pcm") + "/record.wav";
private static DataOutputStream dos;
private static void createFile() {
File pcmFile = new File(path);
if (pcmFile.exists()) {
pcmFile.delete();
}
try {
pcmFile.createNewFile();
dos = new DataOutputStream(new FileOutputStream(pcmFile));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void write(RecordResult recordResult) {
if (recordResult.status == SpeechUtility.START) {
createFile();
} else if (recordResult.status == SpeechUtility.END){
try {
dos.flush();
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
convertWaveFile();
SLog.d("tianzhao","轉碼完成");
}
}).start();
} if (recordResult.status == SpeechUtility.RECOGNIZING){
writeBytes2File(recordResult.recordData);
}
}
private static void writeBytes2File(byte[] recordBytes) {
if (dos == null) {
return;
}
try {
dos.write(recordBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
// 這裏得到可播放的音頻文件
private static void convertWaveFile() {
FileInputStream in = null;
FileOutputStream out = null;
long totalAudioLen = 0;
long totalDataLen = totalAudioLen + 36;
long longSampleRate = 16000;
int channels = 1;
long byteRate = 16 *longSampleRate * channels / 8;
byte[] data = new byte[1280];
try {
in = new FileInputStream(path);
out = new FileOutputStream(wavPath);
totalAudioLen = in.getChannel().size();
//由於不包括RIFF和WAV
totalDataLen = totalAudioLen + 36;
WriteWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/*
任何一種文件在頭部添加相應的頭文件才能夠確定的表示這種文件的格式,wave是RIFF文件結構,每一部分爲一個chunk,其中有RIFF WAVE chunk,
FMT Chunk,Fact chunk,Data chunk,其中Fact chunk是可以選擇的,
*/
private static void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate,
int channels, long byteRate) throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);//數據大小
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W';//WAVE
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
//FMT Chunk
header[12] = 'f'; // 'fmt '
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//過渡字節
//數據大小
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
//編碼方式 10H爲PCM編碼格式
header[20] = 1; // format = 1
header[21] = 0;
//通道數
header[22] = (byte) channels;
header[23] = 0;
//採樣率,每個通道的播放速度
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
//音頻數據傳送速率,採樣率*通道數*採樣深度/8
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 確定系統一次要處理多少個這樣字節的數據,確定緩衝區,通道數*採樣位數
header[32] = (byte) (1 * 16 / 8);
header[33] = 0;
//每個樣本的數據位數
header[34] = 16;
header[35] = 0;
//Data chunk
header[36] = 'd';//data
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
}