一、使用AudioRrecord录音
1.1声明
首先需要声明一个AudioRecord类的实例。之所以需要事先声明,是因为在本例中,录音的启动和结束被封装在两个不同的方法里。而通常来讲,“开始录音”和“结束录音”在大部分时候也确实是需要拆分成两个不同的动作的。
private AudioRecord audioRecord;
除了声明AudioRecord的实例之外,我们还需要准备一些参数:
// 采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
public static final int SAMPLE_RATE_INHZ = 44100;
// 声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
// 返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
这三个参数将被传递给AudioRecord实例,用来进行实例构造。
需要注意的是其中的“采样率”参数:
public static final int SAMPLE_RATE_INHZ = 44100;
本例中采样率取值为“44100”,我们必须在后续的“转换成wav格式”以及“播放”的时候保持这个采样率数值始终一致。
最后,我们还需要准备一个线程类,当Android在使用硬件设备进行录音的时候,我们需要使用一个独立的线程去将Android录取到的数据独立读取并写入一个文件中。
另外我们还需要一个标识,用来告诉这个独立线程什么时候结束录音。
/**
* 录音的工作线程
*/
private Thread recordingAudioThread;
private boolean isRecording = false;//mark if is recording
一切准备工作就绪。
1.2初始化实例
获取录音的缓存大小:
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT);
这里就已经用到我们在之前所准备到的三个参数了。
接下来创建AudioRecord实例,同样也需要用到这三个参数:
this.audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize);
这里有一个要注意的地方,new AudioRecord()的时候是需要获取到运行时权限的。
1.3确认权限
使用try-catch把上述的初始化代码包裹起来,然后在catch中捕获没有权限的异常,并重新申请权限。
这么做的好处是如果应用程序没有权限的话,不会导致当前的界面闪退。
try{
// 获取最小录音缓存大小,
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT);
this.audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize);
//后续的其它代码也将被包裹在这个try中
//...
}
catch(IllegalStateException e){
//需要检查运行时权限
}
catch(SecurityException e){
//需要检查运行时权限
}
关于如何检查运行时权限,请查看此处:
动态获取运行时权限
1.4开始录音
开始录音其实就只有一句代码:
audioRecord.startRecording();
但是在它之前,我们还需要修改我们的录音标识:
// 开始录音
this.isRecording = true;
audioRecord.startRecording();
1.5读取录音数据并保存成文件
我们先要提供一个文件路径:
String audioCacheFilePath = this.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + "jerboa_audio_cache.pcm";
Android录音程序会将录制的声音存为pcm格式,pcm格式是声音原始数据,是无法被播放器识别的。因此在本例的后续步骤里,我们会将它转换成wav格式,因此这里的文件路径实际上就是一个临时的中转路径,所以可以给它一个固定的文件名。
然后就可以使用这个路径在内存里创建出一个文件来:
File file = new File(audioCacheFilePath);
Log.i(TAG, "audio cache pcm file path:" + audioCacheFilePath);
针对这个文件做一些简单的事务准备,并且准备一个输出流:
/*
* 以防万一,看一下这个文件是不是存在,如果存在的话,先删除掉
*/
if (file.exists()) {
file.delete();
}
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e(TAG, "临时缓存文件未找到");
}
if (fos == null) {
return;
}
接下来,准备一个字节数组,用来从Android的录音中读取数据,该字节数组的长度就是先前获取到的缓存大小:
byte[] data = new byte[minBufferSize];
然后就是调用AudioRecord.read()方法,来获取录音数据:
int read;
if (fos != null) {
while (isRecording && !recordingAudioThread.isInterrupted()) {
read = audioRecord.read(data, 0, minBufferSize);
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
fos.write(data);
Log.i("audioRecordTest", "写录音数据->" + read);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
AudioRecord.read()方法会返回一个int值,官方文档是这样描述的:
zero or the positive number of bytes that were read, or one of the following error codes. The number of bytes will not exceed sizeInBytes. ERROR_INVALID_OPERATION if the object isn't properly initialized ERROR_BAD_VALUE if the parameters don't resolve to valid data and indexes ERROR_DEAD_OBJECT if the object is not valid anymore and needs to be recreated. The dead object error code is not returned if some data was successfully transferred. In this case, the error is returned at the next read() ERROR in case of other error
简单来说,如果成功读取到数据,那么这个int值就是读取到的数据的长度。而如果出现了错误,这个int值就是错误编码。
读取完成之后,不要忘记关闭输出流
try {
// 关闭数据流
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
1.5使用线程
实际上,录音是一个持续的过程。AudioRecord类允许我们一边录制一边获取数据。所以我们需要在一个独立的线程内执行上面所有的读取数据的步骤:
// 创建数据流,将缓存导入数据流
this.recordingAudioThread = new Thread(new Runnable() {
@Override
public void run() {
//我们应该在线程内部获取录音数据
//...
}
}
线程必须确保被唤起:
this.recordingAudioThread.start();
1.6完整的封装
我将上面所有的步骤封装成了一个完整的方法:
/**
* 开始录音,返回临时缓存文件(.pcm)的文件路径
*/
protected String startRecordAudio() {
String audioCacheFilePath = this.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + "jerboa_audio_cache.pcm";
try{
// 获取最小录音缓存大小,
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT);
this.audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize);
// 开始录音
this.isRecording = true;
audioRecord.startRecording();
// 创建数据流,将缓存导入数据流
this.recordingAudioThread = new Thread(new Runnable() {
@Override
public void run() {
File file = new File(audioCacheFilePath);
Log.i(TAG, "audio cache pcm file path:" + audioCacheFilePath);
/*
* 以防万一,看一下这个文件是不是存在,如果存在的话,先删除掉
*/
if (file.exists()) {
file.delete();
}
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e(TAG, "临时缓存文件未找到");
}
if (fos == null) {
return;
}
byte[] data = new byte[minBufferSize];
int read;
if (fos != null) {
while (isRecording && !recordingAudioThread.isInterrupted()) {
read = audioRecord.read(data, 0, minBufferSize);
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
fos.write(data);
Log.i("audioRecordTest", "写录音数据->" + read);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
try {
// 关闭数据流
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
this.recordingAudioThread.start();
}
catch(IllegalStateException e){
Log.w(TAG,"需要获取录音权限!");
this.checkIfNeedRequestRunningPermission();
}
catch(SecurityException e){
Log.w(TAG,"需要获取录音权限!");
this.checkIfNeedRequestRunningPermission();
}
return audioCacheFilePath;
}
1.7结束录音
相对于开始录音来说,结束录音要简单得多,我们只需要修改录音标识,然后释放掉AudioRecord实例、中断并取消线程:
/**
* 停止录音
*/
protected void stopRecordAudio(){
try {
this.isRecording = false;
if (this.audioRecord != null) {
this.audioRecord.stop();
this.audioRecord.release();
this.audioRecord = null;
this.recordingAudioThread.interrupt();
this.recordingAudioThread = null;
}
}
catch (Exception e){
Log.w(TAG,e.getLocalizedMessage());
}
}
二、将录音文件保存成wav文件
使用AudioRecord类来获取到的录音文件实际上是pcm文件,pcm文件只是原始的数据,并没有指定任何格式,所以无法被普通的播放器播放。
我们需要把这些原始的音源数据转化成wav文件。
在前面的步骤中,我们已经通过一个指定的文件路径得到了一份pcm文件,现在,我们还需要为即将生成的wav文件来指定一个保存路径:
//wav文件的路径放在系统的音频目录下
String wavFilePath = this.getExternalFilesDir(Environment.DIRECTORY_PODCASTS) + "/wav_" + System.currentTimeMillis() + ".wav";
然后直接调用一个封装好的工具类:
PcmToWavUtil ptwUtil = new PcmToWavUtil();
ptwUtil.pcmToWav("Your ppm file path",wavFilePath,true);
直接上工具类PcmToWavUtil.java:
import android.media.AudioFormat;
import android.media.AudioRecord;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Created by lzt
* time 2021/6/9 15:42
*
* @author lizhengting
* 描述:pcm格式的音频转换为wav格式的工具类
*/
public class PcmToWavUtil {
private int mBufferSize; //缓存的音频大小
private int mSampleRate = 44100;// 此处的值必须与录音时的采样率一致
private int mChannel = AudioFormat.CHANNEL_IN_STEREO; //立体声
private int mEncoding = AudioFormat.ENCODING_PCM_16BIT;
private static class SingleHolder {
static PcmToWavUtil mInstance = new PcmToWavUtil();
}
public static PcmToWavUtil getInstance() {
return SingleHolder.mInstance;
}
public PcmToWavUtil() {
this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, mEncoding);
}
/**
* @param sampleRate sample rate、采样率
* @param channel channel、声道
* @param encoding Audio data format、音频格式
*/
public PcmToWavUtil(int sampleRate, int channel, int encoding) {
this.mSampleRate = sampleRate;
this.mChannel = channel;
this.mEncoding = encoding;
this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, mEncoding);
}
/**
* pcm文件转wav文件
*
* @param inFilename 源文件路径
* @param outFilename 目标文件路径
* @param deleteOrg 是否删除源文件
*/
public void pcmToWav(String inFilename, String outFilename, boolean deleteOrg) {
FileInputStream in;
FileOutputStream out;
long totalAudioLen;
long totalDataLen;
long longSampleRate = mSampleRate;
int channels = 2;
long byteRate = 16 * mSampleRate * channels / 8;
byte[] data = new byte[mBufferSize];
try {
in = new FileInputStream(inFilename);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out, totalAudioLen, totalDataLen,
longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.close();
if (deleteOrg) {
new File(inFilename).delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void pcmToWav(String inFilename, String outFilename) {
pcmToWav(inFilename, outFilename, false);
}
/**
* 加入wav文件头
*/
private 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/WAVE header
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';
header[12] = 'f'; // 'fmt ' chunk
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;
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);
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) (2 * 16 / 8); // block align
header[33] = 0;
header[34] = 16; // bits per sample
header[35] = 0;
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);
}
}
三、使用AudioTrack播放wav文件
播放也比录音要简单许多,直接呈上封装好的代码:
/**
* 播放一个wav文件
*/
protected void playWav(String filePath){
File wavFile = new File(filePath);
try{
FileInputStream fis=new FileInputStream(wavFile);
byte[] buffer=new byte[1024*1024*2];//2M
int len=fis.read(buffer);
Log.i(TAG, "fis len="+len);
Log.i(TAG, "0:"+(char)buffer[0]);
int pcmlen=0;
pcmlen+=buffer[0x2b];
pcmlen=pcmlen*256+buffer[0x2a];
pcmlen=pcmlen*256+buffer[0x29];
pcmlen=pcmlen*256+buffer[0x28];
int channel=buffer[0x17];
channel=channel*256+buffer[0x16];
int bits=buffer[0x23];
bits=bits*256+buffer[0x22];
Log.i(TAG, "pcmlen="+pcmlen+",channel="+channel+",bits="+bits);
AudioTrack at = new AudioTrack(AudioManager.STREAM_MUSIC,
SAMPLE_RATE_INHZ*2,
channel,
AudioFormat.ENCODING_PCM_16BIT,
pcmlen,
AudioTrack.MODE_STATIC);
at.write(buffer, 0x2C, pcmlen);
at.play();
}
catch(Exception e){
}
finally{
wavFile = null;
}
}
四、使用MediaPlayer播放音频
MediaPlayer比AudioTrack更加简单,并且支持传入一个监听器,用来确认播放是否完成:
/**
* 使用MediaPlayer播放文件,并且指定一个当播放完成后会触发的监听器
* @param filePath
* @param onCompletionListener
*/
protected void playWavWithMediaPlayer(String filePath, MediaPlayer.OnCompletionListener onCompletionListener){
//File wavFile = new File(filePath);
try {
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(filePath);
mediaPlayer.setOnCompletionListener(onCompletionListener);
mediaPlayer.prepare();
mediaPlayer.start();
}
catch(Exception e){
}
}
|