在音视频开发中,录音当然是必不可少的。首先我们要学会单独的录音功能,当然这里说的录音是指用AudioRecord来录音,读取录音原始数据,读到的就是所谓的PCM数据。对于录音来说,最重要的几个参数要搞明白:
1、simpleRate采样率,采样率就是采样频率,每秒钟记录多少个样本。
2、channelConfig通道配置,其实就是所谓的单通道,双通道之类的,AudioFormat.CHANNEL_IN_MONO单通道,AudioFormat.CHANNEL_IN_STEREO双通道,这里只列了这两种,还有其它的,可自行查阅。
3、audioFormat音频格式,其实就是采样的精度,每个样本的位数,AudioFormat.ENCODING_PCM_8BIT每个样本占8位,AudioFormat.ENCODING_PCM_16BIT每个样本占16位,这里也只用了这两个,别的没研究。
在学习过程中会用到的一些参数,我这里封装了一个类,如下
public classAudioParams {enumFormat {
SINGLE_8_BIT, DOUBLE_8_BIT, SINGLE_16_BIT, DOUBLE_16_BIT
}privateFormat format;intsimpleRate;
AudioParams(intsimpleRate, Format f) {this.simpleRate =simpleRate;this.format =f;
}
AudioParams(int simpleRate, int channelCount, intbits) {this.simpleRate =simpleRate;
set(channelCount, bits);
}intgetBits() {return (format == Format.SINGLE_8_BIT || format == Format.DOUBLE_8_BIT) ? 8 : 16;
}intgetEncodingFormat() {return (format == Format.SINGLE_8_BIT || format == Format.DOUBLE_8_BIT) ?AudioFormat.ENCODING_PCM_8BIT :
AudioFormat.ENCODING_PCM_16BIT;
}int getChannelCount() {return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? 1 : 2;}intgetChannelConfig() {return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ?AudioFormat.CHANNEL_IN_MONO :
AudioFormat.CHANNEL_IN_STEREO;
}intgetOutChannelConfig() {return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ?AudioFormat.CHANNEL_OUT_MONO :
AudioFormat.CHANNEL_OUT_STEREO;
}void set(int channelCount, intbits) {if ((channelCount != 1 && channelCount != 2) || (bits != 8 && bits != 16)) {throw new IllegalArgumentException("不支持其它格式 channelCount=$channelCount bits=$bits");
}if (channelCount == 1) {if (bits == 8) {
format=Format.SINGLE_8_BIT;
}else{
format=Format.SINGLE_16_BIT;
}
}else{if (bits == 8) {
format=Format.DOUBLE_8_BIT;
}else{
format=Format.DOUBLE_16_BIT;
}
}
}
}
这里固定使用了单通道8位,双通道8位,单通道16位,双通道16位,所以用了枚举来限制。
为了方便把录音数据拿出来显示、存储,这里写了一个回调方法如下
public interfaceRecordCallback {/*** 数据回调
*
*@parambytes 数据
*@paramlen 数据有效长度,-1时表示数据结束*/
void onRecord(byte[] bytes, intlen);
}
有了这些参数,现在就可以录音了,先看一下样例
public voidstartRecord(AudioParams params, RecordCallback callback) {int simpleRate =params.simpleRate;int channelConfig =params.getChannelConfig();int audioFormat =params.getEncodingFormat();//根据AudioRecord提供的api拿到最小缓存大小
int bufferSize =AudioRecord.getMinBufferSize(simpleRate, channelConfig, audioFormat);//创建Record对象
record = newAudioRecord(MediaRecorder.AudioSource.MIC, simpleRate, channelConfig, audioFormat, bufferSize);
recordThread= new Thread(() ->{byte[] buffer = new byte[bufferSize];
record.startRecording();
recording= true;while(recording) {int read = record.read(buffer, 0, bufferSize);//将数据回调到外部
if (read > 0 && callback != null) {
callback.onRecord(buffer, read);
}
}if (callback != null) {//len 为-1时表示结束
callback.onRecord(buffer, -1);
recording= false;
}//释放资源
release();
});
recordThread.start();
}
这个方法就是简单的采集音频数据,这个数据就是最原始的pcm数据。
拿到pcm数据以后,如果直接保存到文件是无法直接播放的,因为这只是一堆数据,没有任何格式说明,如果想让普通播放器可以播放,需要在文件中加入文件头,来告诉播放器这个数据的格式,这里是直接保存成wav格式的数据。下面就是加入wav格式文件头的方法
private static byte[] getWaveFileHeader(int totalDataLen, int sampleRate, int channelCount, intbits) {byte[] header = new byte[44];//RIFF/WAVE header
header[0] = 'R';
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';int fileLength = totalDataLen + 36;
header[4] = (byte) (fileLength & 0xff);
header[5] = (byte) (fileLength >> 8 & 0xff);
header[6] = (byte) (fileLength >> 16 & 0xff);
header[7] = (byte) (fileLength >> 24 & 0xff);//WAVE
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';//'fmt ' chunk
header[12] = 'f';
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//4 bytes: size of 'fmt ' chunk
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;//pcm format = 1
header[20] = 1;
header[21] = 0;
header[22] = (byte) channelCount;
header[23] = 0;
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) (sampleRate >> 8 & 0xff);
header[26] = (byte) (sampleRate >> 16 & 0xff);
header[27] = (byte) (sampleRate >> 24 & 0xff);int byteRate = sampleRate * bits * channelCount / 8;
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) (byteRate >> 8 & 0xff);
header[30] = (byte) (byteRate >> 16 & 0xff);
header[31] = (byte) (byteRate >> 24 & 0xff);//block align
header[32] = (byte) (channelCount * bits / 8);
header[33] = 0;//bits per sample
header[34] = (byte) bits;
header[35] = 0;//data
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalDataLen & 0xff);
header[41] = (byte) (totalDataLen >> 8 & 0xff);
header[42] = (byte) (totalDataLen >> 16 & 0xff);
header[43] = (byte) (totalDataLen >> 24 & 0xff);returnheader;
}
根据几个参数设置一下文件头,然后直接写入录音采集到的pcm数据,就可被正常播放了。wav文件头格式定义,可点击这里查看或自行百度。
如果想要通过AudioRecord录音直接保存到文件,可参考下面方法
public voidstartRecord(String filePath, AudioParams params, RecordCallback callback) {int channelCount =params.getChannelCount();int bits =params.getBits();final boolean storeFile = filePath != null && !filePath.isEmpty();
startRecord(params, (bytes, len)->{if(storeFile) {if (file == null) {
File f= newFile(filePath);if(f.exists()) {
f.delete();
}try{
file= new RandomAccessFile(f, "rw");
file.write(getWaveFileHeader(0, params.simpleRate, channelCount, bits));
}catch(IOException e) {
e.printStackTrace();
}
}if (len > 0) {try{
file.write(bytes,0, len);
}catch(IOException e) {
e.printStackTrace();
}
}else{try{//因为在前面已经写入头信息,所以这里要减去头信息才是数据的长度
int length = (int) file.length() - 44;
file.seek(0);
file.write(getWaveFileHeader(length, params.simpleRate, channelCount, bits));
file.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}if (callback != null) {
callback.onRecord(bytes, len);
}
});
}
先通过RandomAccessFile创建文件,先写入文件头,由于暂时我们不知道会录多长,有多少pcm数据,长度先用0表示,等录音结束后,通过seek(int)方法重新写入文件头信息,也可以先把pcm数据保存到临时文件,然后再写入到一个新的文件中,这里就不举例说明了。
最后放入完整类的代码
packagecn.sskbskdrin.record.audio;importandroid.media.AudioRecord;importandroid.media.MediaRecorder;importjava.io.File;importjava.io.IOException;importjava.io.RandomAccessFile;/***@authorsskbskdrin
* @date 2019/April/3*/
public classAudioRecordManager {private AudioParams DEFAULT_FORMAT = new AudioParams(8000, 1, 16);privateAudioRecord record;privateThread recordThread;private boolean recording = false;privateRandomAccessFile file;public voidstartRecord(String filePath, RecordCallback callback) {
startRecord(filePath, DEFAULT_FORMAT, callback);
}public voidstartRecord(String filePath, AudioParams params, RecordCallback callback) {int channelCount =params.getChannelCount();int bits =params.getBits();final boolean storeFile = filePath != null && !filePath.isEmpty();
startRecord(params, (bytes, len)->{if(storeFile) {if (file == null) {
File f= newFile(filePath);if(f.exists()) {
f.delete();
}try{
file= new RandomAccessFile(f, "rw");
file.write(getWaveFileHeader(0, params.simpleRate, channelCount, bits));
}catch(IOException e) {
e.printStackTrace();
}
}if (len > 0) {try{
file.write(bytes,0, len);
}catch(IOException e) {
e.printStackTrace();
}
}else{try{//因为在前面已经写入头信息,所以这里要减去头信息才是数据的长度
int length = (int) file.length() - 44;
file.seek(0);
file.write(getWaveFileHeader(length, params.simpleRate, channelCount, bits));
file.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}if (callback != null) {
callback.onRecord(bytes, len);
}
});
}public voidstartRecord(AudioParams params, RecordCallback callback) {int simpleRate =params.simpleRate;int channelConfig =params.getChannelConfig();int audioFormat =params.getEncodingFormat();//根据AudioRecord提供的api拿到最小缓存大小
int bufferSize =AudioRecord.getMinBufferSize(simpleRate, channelConfig, audioFormat);//创建Record对象
record = newAudioRecord(MediaRecorder.AudioSource.MIC, simpleRate, channelConfig, audioFormat, bufferSize);
recordThread= new Thread(() ->{byte[] buffer = new byte[bufferSize];
record.startRecording();
recording= true;while(recording) {int read = record.read(buffer, 0, bufferSize);//将数据回调到外部
if (read > 0 && callback != null) {
callback.onRecord(buffer, read);
}
}if (callback != null) {//len 为-1时表示结束
callback.onRecord(buffer, -1);
recording= false;
}//释放资源
release();
});
recordThread.start();
}public voidstop() {
recording= false;
}public voidrelease() {
recording= false;if (record != null) {
record.stop();
record.release();
}
record= null;
file= null;
recordThread= null;
}private static byte[] getWaveFileHeader(int totalDataLen, int sampleRate, int channelCount, intbits) {byte[] header = new byte[44];//RIFF/WAVE header
header[0] = 'R';
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';int fileLength = totalDataLen + 36;
header[4] = (byte) (fileLength & 0xff);
header[5] = (byte) (fileLength >> 8 & 0xff);
header[6] = (byte) (fileLength >> 16 & 0xff);
header[7] = (byte) (fileLength >> 24 & 0xff);//WAVE
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';//'fmt ' chunk
header[12] = 'f';
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//4 bytes: size of 'fmt ' chunk
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;//pcm format = 1
header[20] = 1;
header[21] = 0;
header[22] = (byte) channelCount;
header[23] = 0;
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) (sampleRate >> 8 & 0xff);
header[26] = (byte) (sampleRate >> 16 & 0xff);
header[27] = (byte) (sampleRate >> 24 & 0xff);int byteRate = sampleRate * bits * channelCount / 8;
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) (byteRate >> 8 & 0xff);
header[30] = (byte) (byteRate >> 16 & 0xff);
header[31] = (byte) (byteRate >> 24 & 0xff);//block align
header[32] = (byte) (channelCount * bits / 8);
header[33] = 0;//bits per sample
header[34] = (byte) bits;
header[35] = 0;//data
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalDataLen & 0xff);
header[41] = (byte) (totalDataLen >> 8 & 0xff);
header[42] = (byte) (totalDataLen >> 16 & 0xff);
header[43] = (byte) (totalDataLen >> 24 & 0xff);returnheader;
}public interfaceRecordCallback {/*** 数据回调
*
*@parambytes 数据
*@paramlen 数据有效长度,-1时表示数据结束*/
void onRecord(byte[] bytes, intlen);
}
}
View Code
如有不对之处还请评论指正