Android提供了两个API用于录音的实现:MediaRecorder 和 AudioRecord,各有优劣。
1、MediaRecorder
已经集成了录音、编码、压缩等,支持少量的录音音频格式,大概有.aac(API = 16) .amr .3gp
优点:大部分已经集成,直接调用相关接口即可,代码量小
缺点:无法实时处理音频;输出的音频格式不是很多,例如没有输出mp3格式文件
2、AudioRecord
主要是实现边录边播(AudioRecord+AudioTrack)以及对音频的实时处理(如会说话的汤姆猫、语音)
优点:语音的实时处理,可以用代码实现各种音频的封装
缺点:输出是PCM语音数据,如果保存成音频文件,是不能够被播放器播放的,所以必须先写代码实现数据编码以及压缩
这次我们说说 AudioRecord,由于MediaRecorder 不能实现暂停和继续,在网上看了N多博客等资料,使用了AudioRecord来实现。
大部分代码来自 参考资料:http://blog.csdn.net/imhxl/article/details/52190451
小demo:
AudioRecorder:封装了录音的方法:创建录音对象、开始、暂停、停止、取消,使用静态枚举类Status来记录录音的状态。
FileUtils:文件工具类,用于文件路径的获取
PcmToWav:封装了将.pcm文件转化.wav文件的方法
WaveHeader: wav文件头
RecordStreamListener:监听录音音频流,用于拓展业务的处理
①用于管理录音的工具类
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.text.TextUtils;
import android.text.format.DateFormat;
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.Calendar;
import java.util.List;
import java.util.Locale;
/**
* Created by HXL on 16/8/11.
* 用于实现录音 暂停录音
*/
public class AudioRecorder {
private static AudioRecorder 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 AudioRecorder() {
}
//单例模式
public static AudioRecorder getInstance() {
if (audioRecorder == null) {
audioRecorder = new AudioRecorder();
}
return audioRecorder;
}
/**
* 创建录音对象
*/
public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
channelConfig, audioFormat);
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
this.fileName = fileName;
}
/**
* 创建默认的录音对象
* @param fileName 文件名
*/
public void createDefaultAudio(String fileName) {
mContext = ctx;
mHandler = handler;
// 获得缓冲区字节大小
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;
}
/**
* 开始录音
* @param listener 音频流的监听
*/
public void startRecord(final RecordStreamListener listener) {
if (status == Status.STATUS_NO_READY || TextUtils.isEmpty(fileName)) {
throw new IllegalStateException("录音尚未初始化,请检查是否禁止了录音权限~");
}
if (status == Status.STATUS_START) {
throw new IllegalStateException("正在录音");
}
Log.d("AudioRecorder","===startRecord==="+audioRecord.getState());
audioRecord.startRecording();
new Thread(new Runnable() {
@Override
public void run() {
writeDataTOFile(listener);
}
}).start();
}
/**
* 暂停录音
*/
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;
}
/**
* 将音频信息写入文件
* @param listener 音频流的监听
*/
private void writeDataTOFile(RecordStreamListener listener) {
// new一个byte数组用来存一些字节数据,大小为缓冲区大小
byte[] audiodata = new byte[bufferSizeInBytes];
FileOutputStream fos = null;
int readsize = 0;
try {
String currentFileName = fileName;
if (status == Status.STATUS_PAUSE) {
//假如是暂停录音 将文件名后面加个数字,防止重名文件内容被覆盖
currentFileName += filesName.size();
}
filesName.add(currentFileName);
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());
}
//将录音状态设置成正在录音状态
status = Status.STATUS_START;
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.recordOfByte(audiodata, 0, audiodata.length);
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}
}
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) {
new Thread(new Runnable() {
@Override
public void run() {
if (PcmToWav.mergePCMFilesToWAVFile(filePaths, FileUtils.getWavFileAbsolutePath(fileName))) {
//操作成功
LogUtil.d("录音合成成功");
wavToM4a();
} else {
//操作失败
Log.e("AudioRecorder", "mergePCMFilesToWAVFile fail");
throw new IllegalStateException("mergePCMFilesToWAVFile fail");
}
fileName = null;
}
}).start();
}
/**
* 将单个pcm文件转化为wav文件
*/
private void makePCMFileToWAVFile() {
new Thread(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");
}
fileName = null;
}
}).start();
}
/**
* 获取录音对象的状态
* @return
*/
public Status getStatus() {
return status;
}
/**
* 获取本次录音文件的个数
* @return
*/
public int getPcmFilesCount() {
return filesName.size();
}
/**
* 录音对象的状态
*/
public enum Status {
//未开始
STATUS_NO_READY,
//预备
STATUS_READY,
//录音
STATUS_START,
//暂停
STATUS_PAUSE,
//停止
STATUS_STOP
}
}
② PcmToWAV 将PCM合并转码为WAV格式音频文件
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.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();
}
}
}
}
③WaveHeader类
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);
}
}
}
④RecordStreamListener 扩展接口
/**
* Created by HXL on 16/8/11.
* 获取录音的音频流,用于拓展的处理
*/
public interface RecordStreamListener {
void recordOfByte(byte[] data, int begin, int end);
}
⑤AudioFileUtils工具类
import android.os.Environment;
import android.text.TextUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Created by HXL on 16/8/11.
* 管理录音文件的类
*/
public class AudioFileUtils {
private static String rootPath="audiorecord";
//原始文件(不能播放)
private final static String AUDIO_PCM_BASEPATH = "/"+rootPath+"/pcm/";
//可播放的高质量音频文件
private final static String AUDIO_WAV_BASEPATH = "/"+rootPath+"/wav/";
private static void setRootPath(String rootPath){
FileUtils.rootPath=rootPath;
}
public static String getPcmFileAbsolutePath(String fileName){
if(TextUtils.isEmpty(fileName)){
throw new NullPointerException("fileName isEmpty");
}
if(!isSdcardExit()){
throw new IllegalStateException("sd card no found");
}
String mAudioRawPath = "";
if (isSdcardExit()) {
if (!fileName.endsWith(".pcm")) {
fileName = fileName + ".pcm";
}
String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath() + AUDIO_PCM_BASEPATH;
File file = new File(fileBasePath);
//创建目录
if (!file.exists()) {
file.mkdirs();
}
mAudioRawPath = fileBasePath + fileName;
}
return mAudioRawPath;
}
public static String getWavFileAbsolutePath(String fileName) {
if(fileName==null){
throw new NullPointerException("fileName can't be null");
}
if(!isSdcardExit()){
throw new IllegalStateException("sd card no found");
}
String mAudioWavPath = "";
if (isSdcardExit()) {
if (!fileName.endsWith(".wav")) {
fileName = fileName + ".wav";
}
String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath() + AUDIO_WAV_BASEPATH;
File file = new File(fileBasePath);
//创建目录
if (!file.exists()) {
file.mkdirs();
}
mAudioWavPath = fileBasePath + fileName;
}
return mAudioWavPath;
}
/**
* 判断是否有外部存储设备sdcard
* @return true | false
*/
public static boolean isSdcardExit() {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
return true;
else
return false;
}
/**
* 获取全部pcm文件列表
* @return
*/
public static List<File> getPcmFiles() {
List<File> list = new ArrayList<>();
String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath() + AUDIO_PCM_BASEPATH;
File rootFile = new File(fileBasePath);
if (!rootFile.exists()) {
} else {
File[] files = rootFile.listFiles();
for (File file : files) {
list.add(file);
}
}
return list;
}
/**
* 获取全部wav文件列表
* @return
*/
public static List<File> getWavFiles() {
List<File> list = new ArrayList<>();
String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath() + AUDIO_WAV_BASEPATH;
File rootFile = new File(fileBasePath);
if (!rootFile.exists()) {
} else {
File[] files = rootFile.listFiles();
for (File file : files) {
list.add(file);
}
}
return list;
}
}
⑥在Activity中使用就简单了
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
status = Status.AUDIO_RECORDING;
btnStart.setEnabled(false);
btnPause.setEnabled(true);
btnStop.setEnabled(true);
String fileName = "temp";
audioRecorder.createDefaultAudio(Record3Activity.this,fileName);
audioRecorder.startRecord(null);
timeThread = new Thread(new Runnable() {
@Override
public void run() {
// 记录录音时长并显示
countTime();
}
});
timeThread.start();
}
});
btnPause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btnStart.setEnabled(false);
btnPause.setEnabled(true);
btnStop.setEnabled(true);
if (status == Status.AUDIO_RECORDING) {
status = Status.AUDIO_PUASE;
audioRecorder.pauseRecord();
btnPause.setText("继续");
} else if (status == Status.AUDIO_PUASE) {
status = Status.AUDIO_RECORDING;
audioRecorder.startRecord(null);
btnPause.setText("暂停");
}
}
});
btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
status = Status.AUDIO_STOP;
resetButton();
audioRecorder.stopRecord();
}
});
Status 为自定义的枚举 表示录音状态:正在录音、暂停录音、停止录音。
总结:为了暂停功能踩了N多坑,google大大们也不弄个暂停的 api...
再次感谢:http://blog.csdn.net/imhxl/article/details/52190451 博主的无私分享。