MediaRecorder 和AudioRecord。
参考来自https://www.jianshu.com/p/de779d509e6c
•MediaRecorder:录制的音频文件是经过压缩后的,需要设置编码器。并且录制的音频文件可以用系统自带的Music播放器播放。MediaRecorder已经集成了录音、编码、压缩等,并支持少量的录音音频格式,但是这也是他的缺点,支持的格式过少并且无法实时处理音频数据。
•AudioRecord:主要实现对音频实时处理以及边录边播功能,相对MediaRecorder比较专业,输出是PCM语音数据,如果保存成音频文件,是不能够被播放器播放的,所以必须先写代码实现数据编码以及压缩。
MediaRecorder 使用起来相对简单,音频编码可以根据自己实际需要自己设定,文件名防止重复,使用了日期_时分秒的结构,audioSaveDir 是文件存储目录,可自行设定。
public void startRecord()
{
// 开始录音 /* ①Initial:实例化MediaRecorder对象 */
if (mMediaRecorder == null)mMediaRecorder = new MediaRecorder(); try {
/* ②setAudioSource/setVedioSource */
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 设置麦克风
/* ②设置输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式 ,H263视频/ARM音频编码)、
MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB)*/
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
/* ②设置音频文件的编码:AAC/AMR_NB/AMR_MB/Default 声音的(波形)的采样 */ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
fileName = DateFormat.format("yyyyMMdd_HHmmss", Calendar.getInstance(Locale.CHINA)) + ".m4a";
if (!FileUtils.isFolderExist(FileUtils.getFolderName(audioSaveDir)))
{
FileUtils.makeFolders(audioSaveDir);
}
filePath = audioSaveDir + fileName; /* ③准备 */ mMediaRecorder.setOutputFile(filePath);
mMediaRecorder.prepare(); /* ④开始 */ mMediaRecorder.start();} catch (IllegalStateException e)
{
LogUtil.i("call startAmr(File mRecAudioFile) failed!" + e.getMessage());
}
catch (IOException e) {
LogUtil.i("call startAmr(File mRecAudioFile) failed!" + e.getMessage());
}
}
public void stopRecord()
{
try {
mMediaRecorder.stop();
mMediaRecorder.release();
mMediaRecorder = null;
filePath = "";
}
catch (RuntimeException e) {
LogUtil.e(e.toString());
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null;
File file = new File(filePath);
if (file.exists())file.delete();
filePath = "";
}
}
AndioRecord 类的主要功能是让各种 Java 应用能够管理音频资源,以便它们通过此类能够录制平台的声音输入硬件所收集的声音。此功能的实现就是通过 “pulling 同步”(reading读取)AudioRecord 对象的声音数据来完成的。在录音过程中,应用所需要做的就是通过后面三个类方法中的一个去及时地获取 AudioRecord 对象的录音数据。 AudioRecord 类提供的三个获取声音数据的方法分别是 read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int)。无论选择使用那一个方法都必须事先设定方便用户的声音数据的存储格式。
一个 AudioRecord 需要初始化一个相关联的声音buffer,这个 buffer 主要是用来保存新的声音数据。这个 buffer 的大小,我们可以在对象构造期间去指定。它表明一个 AudioRecord 对象还没有被读取(同步)声音数据前能录多长的音(即一次可以录制的声音容量)。声音数据从音频硬件中被读出,数据大小不超过整个录音数据的大小(可以分多次读出),即每次读取初始化 buffer 容量的数据。
•音频源:我们可以使用麦克风作为采集音频的数据源。
•采样率:一秒钟对声音数据的采样次数,采样率越高,音质越好。
•音频通道:单声道,双声道等,
•音频格式:一般选用PCM格式,即原始的音频样本。
•缓冲区大小:音频数据写入缓冲区的总数,可以通过AudioRecord.getMinBufferSize获取最小的缓冲区。(将音频采集到缓冲区中然后再从缓冲区中读取)。
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; // 音频格式:PCM编码
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 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 canel() {
filesName.clear();
fileName = null;
if (audioRecord != null) {
audioRecord.release();audioRecord = null;
}
status = Status.STATUS_NO_READY;
}
/*** 释放资源 */
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;
}
/*** 将音频信息写入文件 *
@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());
}
}
}
参考来自https://blog.csdn.net/qq77485042/article/details/78416935
添加权限
AndroidManifest.xml中
MediaRecorder mRecorder = new MediaRecorder();
//将mRecorder设置成空闲状态
mRecorder.reset();
//设置要用于录制的音频源。 如果没有这个方法调用,输出文件不会包含音轨。(注意该方法必须在setOutputFormat()方法之前)
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//设置录制时产生的输出文件的格式。(该方法不能再prepare()方法之后调用,不然会抛出IllegalStateException异常)
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
//设置要生成的输出文件的路径。(简单的说就是生成一个什么文件名,放在哪里)
mRecorder.setOutputFile(mFileName);
//我这里的mFileName
mFileName = getExternalCacheDir().getAbsolutePath();
mFileName += "/" + System.currentTimeMillis();
//找到绝对路径,文件名取名为系统当前时间。(这里注意调用setOutputFile()方法一定要在setOutputFormat()方法之后,在prepare()之前);
//设置完路径之后调用该方法
//设置要用于录制的音频编码器。 如果没有这个方法调用,输出文件不会包含音轨。
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
//准备录音机开始捕获和编码数据。(也就是准备状态)这里需要try catch一个IOException。
mRecorder.prepare();
//开始录音
mRecorder.start();
停止的话,调用stop()方法,
release()方法是用来释放掉mRecorder的一切资源,为了不浪费内存。
if (mRecorder != null) {
mRecorder.stop();
mRecorder.release();
mRecorder = null;
}
调用了stop()方法后又重新调用start()方法会发现又生成了一个新的文件。
调用pause()方法暂停
pause()方法是在API 25(安卓版本7.1.1)的时候才出来的,而resume()方法是在API 19(安卓版本4.4)的时候出来的,如果我们的程序需要向下兼容的话怎么办呢,这个暂停的功能不就没法实现了。
他们也是点击了暂停之后调用stop();方法,每次点击开始的时候都生成了一个新的文件,只不过他们把那些录音文件拼接起来当你点击保存的时候拼接成了一个录音文件而已。
首先先创建一个集合用来装每段录音文件的路径
private ArrayList list = new ArrayList<>();
每次点击开始的时候把文件路径add进集合中(也就是上面的mFileName)
因为每次点击开始的时候都给新的文件取当前的时间的毫秒值,所以不会出现文件名重复而覆盖掉的情况。
mFileName = getExternalCacheDir().getAbsolutePath();
mFileName += "/" + System.currentTimeMillis();
list.add(mFileName);
startRecording();
/**
* 开始录音
*/
private void startRecording() {
if (mRecorder == null) {
mRecorder = new MediaRecorder();
}
mRecorder.reset();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
mRecorder.setOutputFile(mFileName);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
try {
mRecorder.prepare();
} catch (IOException e) {
Log.e(TAG, e.toString());
}
mRecorder.start();
}
传入list以保存
第一个参数就是list集合,第二个参数就是合并后的文件名
public void getInputCollection(List list, String mMinute1) {
// 创建音频文件,合并的文件放这里
file1 = new File(getExternalCacheDir().getAbsolutePath(), mMinute1);
FileOutputStream fileOutputStream = null;
if (!file1.exists()) {
try {
file1.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
fileOutputStream = new FileOutputStream(file1);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//list里面为暂停录音 所产生的 几段录音文件的名字,中间几段文件的减去前面的6个字节头文件
for (int i = 0; i < list.size(); i++) {
File file = new File((String) list.get(i));
Log.d("list的长度", list.size() + "");
try {
FileInputStream fileInputStream = new FileInputStream(file);
byte[] myByte = new byte[fileInputStream.available()];
//文件长度
int length = myByte.length;
//头文件
if (i == 0) {
while (fileInputStream.read(myByte) != -1) {
fileOutputStream.write(myByte, 0, length);
}
}
//之后的文件,去掉头文件就可以了
else {
while (fileInputStream.read(myByte) != -1) {
fileOutputStream.write(myByte, 6, length - 6);
}
}
fileOutputStream.flush();
fileInputStream.close();
System.out.println("合成文件长度:" + file1.length());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//结束后关闭流
try {
fileOutputStream.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//千万记住这里需要把集合给清空不然,下次录的时候会把上次录的给带上了
deleteListRecord();
}
/**
* 合成一个文件后,删除之前暂停录音所保存的零碎合成文件
*/
private void deleteListRecord() {
for (int i = 0; i < list.size(); i++) {
File file = new File(list.get(i));
if (file.exists()) {
file.delete();
}
}
//正在暂停后,继续录音的这一段音频文件
}
安卓6.0以上需要动态获取权限,在MainActivity中加入以下代码
if (Build.VERSION.SDK_INT >= 23) {
int REQUEST_CODE_CONTACT = 101;
String[] permissions = {Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
//验证是否许可权限
for (String str : permissions) {
if (this.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
//申请权限
this.requestPermissions(permissions, REQUEST_CODE_CONTACT);
return;
}
}
}