題外話:發現好久都沒有上來寫博文了,畢業設計加上公司暫時沒有Android的項目做,只能去自學web上的知識,摸爬打滾到現在,花了一個多月時間根據公司的現有模板做了公司內部一個任務管理系統,感覺都是比較淺的知識,沒什麼可以寫的。想到之前做的語音識別的項目,雖然現在沒什麼下文了,但是誰懂~~~將來呢?
言歸正傳,項目長這樣子:
設計的思路:
由於自帶的AudioRecord沒有pauseRecord()方法,我把開始錄音-->(暫停/繼續錄音)...-->停止錄音叫做一次錄音,點擊一次暫停就會產生一個文件(.pcm),將一次錄音產生的所有文件名(.pcm)用一個list裝起來,點擊停止後將遍歷list取得所有文件路徑進行拼接。
由於考慮到以後可能要進行語音識別,所以對程序的靈活性和拓展性都做了相應的處理,可以通過setListener()監聽錄音的音頻流和監聽錄音結束。
採用線程池對線程進行管理,減少系統開銷。
對類的說明:
AudioRecorder:封裝了錄音的方法:創建錄音對象、開始、暫停、停止、取消,使用靜態枚舉類Status來記錄錄音的狀態。
FileUtils:文件工具類,用於文件路徑的獲取
PcmToWav:封裝了將.pcm文件轉化.wav文件的方法
WaveHeader: wav文件頭
RecordStreamListener:監聽錄音音頻流,用於拓展業務的處理
接下來是關鍵代碼部分:
1、AudioRecorder類:
package com.hxl.pauserecord.record;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by HXL on 16/8/11.
* 用於實現錄音 暫停錄音
*/
public class AudioRecorder {
//音頻輸入-麥克風
private final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
//採用頻率
//44100是目前的標準,但是某些設備仍然支持22050,16000,11025
//採樣頻率一般共分爲22.05KHz、44.1KHz、48KHz三個等級
private final static int AUDIO_SAMPLE_RATE = 16000;
//聲道 單聲道
private final static int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
//編碼
private final static int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
// 緩衝區字節大小
private int bufferSizeInBytes = 0;
//錄音對象
private AudioRecord audioRecord;
//錄音狀態
private Status status = Status.STATUS_NO_READY;
//文件名
private String fileName;
//錄音文件
private List<String> filesName = new ArrayList<>();
//線程池
private ExecutorService mExecutorService;
//錄音監聽
private RecordStreamListener listener;
public AudioRecorder() {
mExecutorService = Executors.newCachedThreadPool();
}
/**
* 創建錄音對象
*/
public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
// 獲得緩衝區字節大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
channelConfig, channelConfig);
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
this.fileName = fileName;
}
/**
* 創建默認的錄音對象
*
* @param fileName 文件名
*/
public void createDefaultAudio(String fileName) {
// 獲得緩衝區字節大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL, AUDIO_ENCODING);
audioRecord = new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
this.fileName = fileName;
status = Status.STATUS_READY;
}
/**
* 開始錄音
*
*/
public void startRecord() {
if (status == Status.STATUS_NO_READY||audioRecord==null) {
throw new IllegalStateException("錄音尚未初始化,請檢查是否禁止了錄音權限~");
}
if (status == Status.STATUS_START) {
throw new IllegalStateException("正在錄音");
}
Log.d("AudioRecorder", "===startRecord===" + audioRecord.getState());
audioRecord.startRecording();
String currentFileName = fileName;
if (status == Status.STATUS_PAUSE) {
//假如是暫停錄音 將文件名後面加個數字,防止重名文件內容被覆蓋
currentFileName += filesName.size();
}
filesName.add(currentFileName);
final String finalFileName=currentFileName;
//將錄音狀態設置成正在錄音狀態
status = Status.STATUS_START;
//使用線程池管理線程
mExecutorService.execute(new Runnable() {
@Override
public void run() {
writeDataTOFile(finalFileName);
}
});
}
/**
* 暫停錄音
*/
public void pauseRecord() {
Log.d("AudioRecorder", "===pauseRecord===");
if (status != Status.STATUS_START) {
throw new IllegalStateException("沒有在錄音");
} else {
audioRecord.stop();
status = Status.STATUS_PAUSE;
}
}
/**
* 停止錄音
*/
public void stopRecord() {
Log.d("AudioRecorder", "===stopRecord===");
if (status == Status.STATUS_NO_READY || status == Status.STATUS_READY) {
throw new IllegalStateException("錄音尚未開始");
} else {
audioRecord.stop();
status = Status.STATUS_STOP;
release();
}
}
/**
* 釋放資源
*/
public void release() {
Log.d("AudioRecorder", "===release===");
//假如有暫停錄音
try {
if (filesName.size() > 0) {
List<String> filePaths = new ArrayList<>();
for (String fileName : filesName) {
filePaths.add(FileUtils.getPcmFileAbsolutePath(fileName));
}
//清除
filesName.clear();
//將多個pcm文件轉化爲wav文件
mergePCMFilesToWAVFile(filePaths);
} else {
//這裏由於只要錄音過filesName.size都會大於0,沒錄音時fileName爲null
//會報空指針 NullPointerException
// 將單個pcm文件轉化爲wav文件
//Log.d("AudioRecorder", "=====makePCMFileToWAVFile======");
//makePCMFileToWAVFile();
}
} catch (IllegalStateException e) {
throw new IllegalStateException(e.getMessage());
}
if (audioRecord != null) {
audioRecord.release();
audioRecord = null;
}
status = Status.STATUS_NO_READY;
}
/**
* 取消錄音
*/
public void canel() {
filesName.clear();
fileName = null;
if (audioRecord != null) {
audioRecord.release();
audioRecord = null;
}
status = Status.STATUS_NO_READY;
}
/**
* 將音頻信息寫入文件
*
*/
private void writeDataTOFile(String currentFileName) {
// new一個byte數組用來存一些字節數據,大小爲緩衝區大小
byte[] audiodata = new byte[bufferSizeInBytes];
FileOutputStream fos = null;
int readsize = 0;
try {
File file = new File(FileUtils.getPcmFileAbsolutePath(currentFileName));
if (file.exists()) {
file.delete();
}
fos = new FileOutputStream(file);// 建立一個可存取字節的文件
} catch (IllegalStateException e) {
Log.e("AudioRecorder", e.getMessage());
throw new IllegalStateException(e.getMessage());
} catch (FileNotFoundException e) {
Log.e("AudioRecorder", e.getMessage());
}
while (status == Status.STATUS_START) {
readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) {
try {
fos.write(audiodata);
if (listener != null) {
//用於拓展業務
listener.onRecording(audiodata, 0, audiodata.length);
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}
}
if (listener != null) {
listener.finishRecord();
}
try {
if (fos != null) {
fos.close();// 關閉寫入流
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}
/**
* 將pcm合併成wav
*
* @param filePaths
*/
private void mergePCMFilesToWAVFile(final List<String> filePaths) {
mExecutorService.execute(new Runnable() {
@Override
public void run() {
if (PcmToWav.mergePCMFilesToWAVFile(filePaths, FileUtils.getWavFileAbsolutePath(fileName))) {
//操作成功
} else {
//操作失敗
Log.e("AudioRecorder", "mergePCMFilesToWAVFile fail");
throw new IllegalStateException("mergePCMFilesToWAVFile fail");
}
}
});
}
/**
* 將單個pcm文件轉化爲wav文件
*/
private void makePCMFileToWAVFile() {
mExecutorService.execute(new Runnable() {
@Override
public void run() {
if (PcmToWav.makePCMFileToWAVFile(FileUtils.getPcmFileAbsolutePath(fileName), FileUtils.getWavFileAbsolutePath(fileName), true)) {
//操作成功
} else {
//操作失敗
Log.e("AudioRecorder", "makePCMFileToWAVFile fail");
throw new IllegalStateException("makePCMFileToWAVFile fail");
}
}
});
}
/**
* 錄音對象的狀態
*/
public enum Status {
//未開始
STATUS_NO_READY,
//預備
STATUS_READY,
//錄音
STATUS_START,
//暫停
STATUS_PAUSE,
//停止
STATUS_STOP
}
/**
* 獲取錄音對象的狀態
*
* @return
*/
public Status getStatus() {
return status;
}
/**
* 獲取本次錄音文件的個數
*
* @return
*/
public int getPcmFilesCount() {
return filesName.size();
}
public RecordStreamListener getListener() {
return listener;
}
public void setListener(RecordStreamListener listener) {
this.listener = listener;
}
}
2:PcmToWav
package com.hxl.pauserecord.record;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Created by HXL on 16/8/11.
* 將pcm文件轉化爲wav文件
*/
public class PcmToWav {
/**
* 合併多個pcm文件爲一個wav文件
*
* @param filePathList pcm文件路徑集合
* @param destinationPath 目標wav文件路徑
* @return true|false
*/
public static boolean mergePCMFilesToWAVFile(List<String> filePathList,
String destinationPath) {
File[] file = new File[filePathList.size()];
byte buffer[] = null;
int TOTAL_SIZE = 0;
int fileNum = filePathList.size();
for (int i = 0; i < fileNum; i++) {
file[i] = new File(filePathList.get(i));
TOTAL_SIZE += file[i].length();
}
// 填入參數,比特率等等。這裏用的是16位單聲道 8000 hz
WaveHeader header = new WaveHeader();
// 長度字段 = 內容的大小(TOTAL_SIZE) +
// 頭部字段的大小(不包括前面4字節的標識符RIFF以及fileLength本身的4字節)
header.fileLength = TOTAL_SIZE + (44 - 8);
header.FmtHdrLeth = 16;
header.BitsPerSample = 16;
header.Channels = 2;
header.FormatTag = 0x0001;
header.SamplesPerSec = 8000;
header.BlockAlign = (short) (header.Channels * header.BitsPerSample / 8);
header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
header.DataHdrLeth = TOTAL_SIZE;
byte[] h = null;
try {
h = header.getHeader();
} catch (IOException e1) {
Log.e("PcmToWav", e1.getMessage());
return false;
}
if (h.length != 44) // WAV標準,頭部應該是44字節,如果不是44個字節則不進行轉換文件
return false;
//先刪除目標文件
File destfile = new File(destinationPath);
if (destfile.exists())
destfile.delete();
//合成所有的pcm文件的數據,寫到目標文件
try {
buffer = new byte[1024 * 4]; // Length of All Files, Total Size
InputStream inStream = null;
OutputStream ouStream = null;
ouStream = new BufferedOutputStream(new FileOutputStream(
destinationPath));
ouStream.write(h, 0, h.length);
for (int j = 0; j < fileNum; j++) {
inStream = new BufferedInputStream(new FileInputStream(file[j]));
int size = inStream.read(buffer);
while (size != -1) {
ouStream.write(buffer);
size = inStream.read(buffer);
}
inStream.close();
}
ouStream.close();
} catch (FileNotFoundException e) {
Log.e("PcmToWav", e.getMessage());
return false;
} catch (IOException ioe) {
Log.e("PcmToWav", ioe.getMessage());
return false;
}
clearFiles(filePathList);
Log.i("PcmToWav", "mergePCMFilesToWAVFile success!" + new SimpleDateFormat("yyyy-MM-dd hh:mm").format(new Date()));
return true;
}
/**
* 將一個pcm文件轉化爲wav文件
*
* @param pcmPath pcm文件路徑
* @param destinationPath 目標文件路徑(wav)
* @param deletePcmFile 是否刪除源文件
* @return
*/
public static boolean makePCMFileToWAVFile(String pcmPath, String destinationPath, boolean deletePcmFile) {
byte buffer[] = null;
int TOTAL_SIZE = 0;
File file = new File(pcmPath);
if (!file.exists()) {
return false;
}
TOTAL_SIZE = (int) file.length();
// 填入參數,比特率等等。這裏用的是16位單聲道 8000 hz
WaveHeader header = new WaveHeader();
// 長度字段 = 內容的大小(TOTAL_SIZE) +
// 頭部字段的大小(不包括前面4字節的標識符RIFF以及fileLength本身的4字節)
header.fileLength = TOTAL_SIZE + (44 - 8);
header.FmtHdrLeth = 16;
header.BitsPerSample = 16;
header.Channels = 2;
header.FormatTag = 0x0001;
header.SamplesPerSec = 8000;
header.BlockAlign = (short) (header.Channels * header.BitsPerSample / 8);
header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
header.DataHdrLeth = TOTAL_SIZE;
byte[] h = null;
try {
h = header.getHeader();
} catch (IOException e1) {
Log.e("PcmToWav", e1.getMessage());
return false;
}
if (h.length != 44) // WAV標準,頭部應該是44字節,如果不是44個字節則不進行轉換文件
return false;
//先刪除目標文件
File destfile = new File(destinationPath);
if (destfile.exists())
destfile.delete();
//合成所有的pcm文件的數據,寫到目標文件
try {
buffer = new byte[1024 * 4]; // Length of All Files, Total Size
InputStream inStream = null;
OutputStream ouStream = null;
ouStream = new BufferedOutputStream(new FileOutputStream(
destinationPath));
ouStream.write(h, 0, h.length);
inStream = new BufferedInputStream(new FileInputStream(file));
int size = inStream.read(buffer);
while (size != -1) {
ouStream.write(buffer);
size = inStream.read(buffer);
}
inStream.close();
ouStream.close();
} catch (FileNotFoundException e) {
Log.e("PcmToWav", e.getMessage());
return false;
} catch (IOException ioe) {
Log.e("PcmToWav", ioe.getMessage());
return false;
}
if (deletePcmFile) {
file.delete();
}
Log.i("PcmToWav", "makePCMFileToWAVFile success!" + new SimpleDateFormat("yyyy-MM-dd hh:mm").format(new Date()));
return true;
}
/**
* 清除文件
*
* @param filePathList
*/
private static void clearFiles(List<String> filePathList) {
for (int i = 0; i < filePathList.size(); i++) {
File file = new File(filePathList.get(i));
if (file.exists()) {
file.delete();
}
}
}
}
3、WaveHeader類:
package com.hxl.pauserecord.record;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Created by HXL on 16/3/9.
* wav文件頭
*/
public class WaveHeader {
public final char fileID[] = {'R', 'I', 'F', 'F'};
public int fileLength;
public char wavTag[] = {'W', 'A', 'V', 'E'};;
public char FmtHdrID[] = {'f', 'm', 't', ' '};
public int FmtHdrLeth;
public short FormatTag;
public short Channels;
public int SamplesPerSec;
public int AvgBytesPerSec;
public short BlockAlign;
public short BitsPerSample;
public char DataHdrID[] = {'d','a','t','a'};
public int DataHdrLeth;
public byte[] getHeader() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
WriteChar(bos, fileID);
WriteInt(bos, fileLength);
WriteChar(bos, wavTag);
WriteChar(bos, FmtHdrID);
WriteInt(bos,FmtHdrLeth);
WriteShort(bos,FormatTag);
WriteShort(bos,Channels);
WriteInt(bos,SamplesPerSec);
WriteInt(bos,AvgBytesPerSec);
WriteShort(bos,BlockAlign);
WriteShort(bos,BitsPerSample);
WriteChar(bos,DataHdrID);
WriteInt(bos,DataHdrLeth);
bos.flush();
byte[] r = bos.toByteArray();
bos.close();
return r;
}
private void WriteShort(ByteArrayOutputStream bos, int s) throws IOException {
byte[] mybyte = new byte[2];
mybyte[1] =(byte)( (s << 16) >> 24 );
mybyte[0] =(byte)( (s << 24) >> 24 );
bos.write(mybyte);
}
private void WriteInt(ByteArrayOutputStream bos, int n) throws IOException {
byte[] buf = new byte[4];
buf[3] =(byte)( n >> 24 );
buf[2] =(byte)( (n << 8) >> 24 );
buf[1] =(byte)( (n << 16) >> 24 );
buf[0] =(byte)( (n << 24) >> 24 );
bos.write(buf);
}
private void WriteChar(ByteArrayOutputStream bos, char[] id) {
for (int i=0; i<id.length; i++) {
char c = id[i];
bos.write(c);
}
}
}
接下來是效果圖。。。個人爲人做APP界面一定要美觀,而且要非常美觀,不然誰會用你的東西!so~~
好吧,請大家撇開UI看功能~正如預期的一樣,每點擊暫停一次會生成一個pcm文件,當點擊停止的時候,將所有的錄音整合成一個可播放的.wav文件
接下來,項目源碼:點擊下載
結語:
有什麼寫得不對,或者可以優化的地方,歡迎大家指正交流,謝謝大家~~