前言
Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。
实现流程
- 获取权限
- 初始化获取每一帧流的Size
- 初始化音频录制AudioRecord
- 开始录制与保存录制音频文件
- 停止录制
- 给音频文件添加头部信息,并且转换格式成wav
- 释放AudioRecord,录制流程完毕
获取权限
如果是Android5.0以上,以上3个权限需要动态授权<!--音频录制权限 --><uses-permission android:name="android.permission.RECORD_AUDIO" /><!--读取和写入存储权限--><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
初始化获取每一帧流的Size
private Integer mRecordBufferSize;private void initMinBufferSize(){//获取每一帧的字节流大小mRecordBufferSize = AudioRecord.getMinBufferSize(8000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);}
第一个参数sampleRateInHz 采样率(赫兹),方法注释里有说明
只能在4000到192000的范围内取值
在AudioFormat类里public static final int SAMPLE_RATE_HZ_MIN = 4000; 最小4000public static final int SAMPLE_RATE_HZ_MAX = 192000; 最大192000
第二个参数channelConfig 声道配置 描述音频声道的配置,例如左声道/右声道/前声道/后声道。
在AudioFormat类录public static final int CHANNEL_IN_LEFT = 0x4;//左声道public static final int CHANNEL_IN_RIGHT = 0x8;//右声道public static final int CHANNEL_IN_FRONT = 0x10;//前声道public static final int CHANNEL_IN_BACK = 0x20;//后声道public static final int CHANNEL_IN_LEFT_PROCESSED = 0x40;public static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80;public static final int CHANNEL_IN_FRONT_PROCESSED = 0x100;public static final int CHANNEL_IN_BACK_PROCESSED = 0x200;public static final int CHANNEL_IN_PRESSURE = 0x400;public static final int CHANNEL_IN_X_AXIS = 0x800;public static final int CHANNEL_IN_Y_AXIS = 0x1000;public static final int CHANNEL_IN_Z_AXIS = 0x2000;public static final int CHANNEL_IN_VOICE_UPLINK = 0x4000;public static final int CHANNEL_IN_VOICE_DNLINK = 0x8000;public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;//单声道public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);//立体声道(左右声道)
第三个参数audioFormat 音频格式 表示音频数据的格式。
注意!一般的手机设备可能只支持 16位PCM编码,如果其他的都会报错为坏值.public static final int ENCODING_PCM_16BIT = 2; //16位PCM编码public static final int ENCODING_PCM_8BIT = 3; //8位PCM编码public static final int ENCODING_PCM_FLOAT = 4; //4位PCM编码public static final int ENCODING_AC3 = 5;public static final int ENCODING_E_AC3 = 6;public static final int ENCODING_DTS = 7;public static final int ENCODING_DTS_HD = 8;public static final int ENCODING_MP3 = 9; //MP3编码 此格式可能会因为不设备不支持报错public static final int ENCODING_AAC_LC = 10;public static final int ENCODING_AAC_HE_V1 = 11;public static final int ENCODING_AAC_HE_V2 = 12;
初始化音频录制AudioRecord
private AudioRecord mAudioRecord;private void initAudioRecord(){mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 8000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, mRecordBufferSize);}
- 第一个参数audioSource 音频源 这里选择使用麦克风:MediaRecorder.AudioSource.MIC
- 第二个参数sampleRateInHz 采样率(赫兹) 与前面初始化获取每一帧流的Size保持一致
- 第三个参数channelConfig 声道配置 描述音频声道的配置,例如左声道/右声道/前声道/后声道。 与前面初始化获取每一帧流的Size保持一致
- 第四个参数audioFormat 音频格式 表示音频数据的格式。 与前面初始化获取每一帧流的Size保持一致
- 第五个参数缓存区大小,就是上面我们配置的AudioRecord.getMinBufferSize
开始录制与保存录制音频文件
这里说明一下为什么用布尔值,来关闭录制.有些小伙伴会发现AudioRecord是可以获取到录制状态的.那么肯定有人会用状态来判断while是否还需要处理流.这种是错误的做法.因为MIC属于硬件层任何硬件的东西都是异步的而且会有很大的延时.所以回调的状态也是有延时的,有时候流没了,但是状态还是显示为正在录制.private boolean mWhetherRecord;private File pcmFile;private void startRecord(){pcmFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord.pcm");mWhetherRecord = true;new Thread(new Runnable() {@Overridepublic void run() {mAudioRecord.startRecording();//开始录制FileOutputStream fileOutputStream = null;try {fileOutputStream = new FileOutputStream(pcmFile);byte[] bytes = new byte[mRecordBufferSize];while (mWhetherRecord){mAudioRecord.read(bytes, 0, bytes.length);//读取流fileOutputStream.write(bytes);fileOutputStream.flush();}Log.e(TAG, "run: 暂停录制" );mAudioRecord.stop();//停止录制fileOutputStream.flush();fileOutputStream.close();addHeadData();//添加音频头部信息并且转成wav格式} catch (FileNotFoundException e) {e.printStackTrace();mAudioRecord.stop();} catch (IOException e) {e.printStackTrace();}}}).start();}
停止录制
就是调用mAudioRecord.stop();方法来停止录制,但是因为我在上面的保存流后做了调用停止视频录制,所以我这里只需要切换布尔值就可以关闭音频录制private void stopRecord(){mWhetherRecord = false;}
给音频文件添加头部信息,并且转换格式成wav
音频录制完成后,这个时候去存储目录找到音频文件部分,会提示无法播放文件.其实是因为没有加入音频头部信息.一般通过麦克风采集的录音数据都是PCM格式的,即不包含头部信息,播放器无法知道音频采样率、位宽等参数,导致无法播放,显然是非常不方便的。pcm转换成wav,我们只需要在pcm的文件起始位置加上至少44个字节的WAV头信息即可。**
| 偏移地址 | 命名 | 内容 |
|---|---|---|
| 00-03 | ChunkId | “RIFF” |
| 04-07 | ChunkSize | 下个地址开始到文件尾的总字节数(此Chunk的数据大小) |
| 08-11 | fccType | “WAVE” |
| 12-15 | SubChunkId1 | “fmt “,最后一位空格。 |
| 16-19 | SubChunkSize1 | 一般为16,表示fmt Chunk的数据块大小为16字节 |
| 20-21 | FormatTag | 1:表示是PCM 编码 |
| 22-23 | Channels | 声道数,单声道为1,双声道为2 |
| 24-27 | SamplesPerSec | 采样率 |
| 28-31 | BytesPerSec | 码率 :采样率 采样位数 声道个数, bytePerSecond = sampleRate (bitsPerSample / 8) channels |
| 32-33 | BlockAlign | 每次采样的大小:位宽*声道数/8 |
| 34-35 | BitsPerSample | 位宽 |
| 36-39 | SubChunkId2 | “data” |
| 40-43 | SubChunkSize2 | 音频数据的长度 |
| 44-… | data | 音频数据 |
private void addHeadData(){pcmFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord.pcm");handlerWavFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord_handler.wav");PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(8000,AudioFormat.CHANNEL_IN_MONO,AudioFormat.ENCODING_PCM_16BIT);pcmToWavUtil.pcmToWav(pcmFile.toString(),handlerWavFile.toString());}
写入头部信息的工具类
注意输入File和输出File不能同一个,因为没有做缓存.
public class PcmToWavUtil {private static final String TAG = "PcmToWavUtil";/** 缓存的音频大小 */private int mBufferSize;/** 采样率 */private int mSampleRate;/** 声道数 */private int mChannel;/*** @param sampleRate sample rate、采样率* @param channel channel、声道* @param encoding Audio data format、音频格式*/PcmToWavUtil(int sampleRate, int channel, int encoding) {this.mSampleRate = sampleRate;this.mChannel = channel;this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);}/*** pcm文件转wav文件** @param inFilename 源文件路径* @param outFilename 目标文件路径*/public void pcmToWav(String inFilename, String outFilename) {FileInputStream in;FileOutputStream out;long totalAudioLen;//总录音长度long totalDataLen;//总数据长度long longSampleRate = mSampleRate;int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 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);out.flush();}Log.e(TAG, "pcmToWav: 停止处理");in.close();out.close();} catch (IOException e) {e.printStackTrace();}}/*** 加入wav文件头*/private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,long totalDataLen, long longSampleRate, int channels, long byteRate)throws IOException {byte[] header = new byte[44];// RIFF/WAVE headerheader[0] = 'R';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);//WAVEheader[8] = 'W';header[9] = 'A';header[10] = 'V';header[11] = 'E';// 'fmt ' chunkheader[12] = 'f';header[13] = 'm';header[14] = 't';header[15] = ' ';// 4 bytes: size of 'fmt ' chunkheader[16] = 16;header[17] = 0;header[18] = 0;header[19] = 0;// format = 1header[20] = 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);// block alignheader[32] = (byte) (2 * 16 / 8);header[33] = 0;// bits per sampleheader[34] = 16;header[35] = 0;//dataheader[36] = 'd';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);}}
释放AudioRecord,录制流程完毕
调用release()方法释放资源
mAudioRecord.release();
最后介绍下其他API
获取AudioRecord初始化状态
public int getState() {return mState;}
注意!这里是初始化状态,不是录制状态,它只会返回2个状态
