Android 录音MediaRecorder到AudioRecord

研究录音是源于即时通讯的项目。写出一个即时通讯很简单,但是写好一个即时通讯就不是一件容易的事,比如聊天中语音的加入。接下来就来描述一下自己对语音的见解和处理方式。

首先写到语音,当然首当其冲的是运用到网上百分之八九十的处理方案MediaRecorder,这个也是我首先用到的方式,主要是由于以前写过接入环信的项目,它里面所提供的就是MediaRecorder。大概的来介绍一下这个类的几个常用方法吧。

Initial:初始状态,当使用new()方法创建一个MediaRecorder对象或者调用了reset()方法时,该MediaRecorder对象处于Initial状态。在设定视频源或者音频源之后将转换为Initialized状态。另外,在除Released状态外的其它状态通过调用reset()方法都可以使MediaRecorder进入该状态。

Initialized:已初始化状态,可以通过在Initial状态调用setAudioSource()或setVideoSource()方法进入该状态。在这个状态可以通过setOutputFormat()方法设置输出格式,此时MediaRecorder转换为DataSourceConfigured状态。另外,通过reset()方法进入Initial状态。

DataSourceConfigured:数据源配置状态,这期间可以设定编码方式、输出文件、屏幕旋转、预览显示等等。可以在Initialized状态通过setOutputFormat()方法进入该状态。另外,可以通过reset()方法回到Initial状态,或者通过prepare()方法到达Prepared状态。

Prepared:就绪状态,在DataSourceConfigured状态通过prepare()方法进入该状态。在这个状态可以通过start()进入录制状态。另外,可以通过reset()方法回到Initialized状态。

Recording:录制状态,可以在Prepared状态通过调用start()方法进入该状态。另外,它可以通过stop()方法或reset()方法回到Initial状态。

Released:释放状态(官方文档给出的词叫做Idle state 空闲状态),可以通过在Initial状态调用release()方法来进入这个状态,这时将会释放所有和MediaRecorder对象绑定的资源。

Error:错误状态,当错误发生的时候进入这个状态,它可以通过reset()方法进入Initial状态

这个MediaRecorder的主要是有停供的方法来进行编码语音,好处自然就是方便,另外一个就是它所编译的语音文件体积非常小。大概有AAC和ARM用得比较多,环信使用的就是ARM的编码方式。其实我觉得差别不是很大,都不是特别好的录音,对于项目要求不高的可以考虑下,下面我也会粘贴出工具类,方便使用(EaseVoiceRecorder),对了,忘了说,它还提供通道设置setAudioChannels  1是单声道 2是多声道;setAudioSamplingRate采样率,网上说的是越大越好,但是我选了很多中个人觉得16000比较适中。

importjava.io.File;

importjava.io.IOException;

importjava.util.Date;

importandroid.content.Context;

importandroid.content.pm.PackageManager;

importandroid.media.MediaRecorder;

importandroid.os.Handler;

importandroid.os.SystemClock;

importandroid.text.format.Time;

importandroid.util.Log;

importcom.lvgou.distribution.driect.entity.EMError;

importcom.lvgou.distribution.utils.PathUtil;

publicclassEaseVoiceRecorder{

MediaRecorderrecorder;

staticfinalStringPREFIX="voice";

staticfinalStringEXTENSION=".mp3";

Stringuid;

privatebooleanisRecording=false;

privatelongstartTime=-4;

privateStringvoiceFilePath=null;

privateStringvoiceFileName=null;

privateFilefile;

privateHandlerhandler;

privateContextmContext;

//    public EaseVoiceRecorder(Handler handler) {

//        this.handler = handler;

//    }

publicEaseVoiceRecorder(){

}

/**

* @param appContext

* @param userId    传入userId 用于标示 名称

* @return

*/

publicStringstartRecording(ContextappContext,StringuserId){

mContext=appContext;

file=null;

startTime=-4;

try{

// need to create recorder every time, otherwise, will got exception

// from setOutputFile when try to reuse

if(recorder!=null){

recorder.release();

recorder=null;

}

recorder=newMediaRecorder();

recorder.setAudioSource(MediaRecorder.AudioSource.MIC);

//            recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);

//            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

//            recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

//            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

/*//          方案一

recorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);

recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

recorder.setAudioChannels(2); // MONO

recorder.setAudioSamplingRate(16000); // 8000Hz*/

//          方案二

recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

recorder.setAudioChannels(2);

recorder.setAudioSamplingRate(16000);

/*//            方案三

recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);

recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);*/

//            recorder.setAudioEncodingBitRate(64); // seems if change this to

// 128, still got same file

// size.

// one easy way is to use temp file

// file = File.createTempFile(PREFIX + userId, EXTENSION,

// User.getVoicePath());

voiceFileName=getVoiceFileName(userId);

voiceFilePath=PathUtil.getInstance().getVoicePath()+"/"+voiceFileName;

file=newFile(voiceFilePath);

recorder.setOutputFile(file.getAbsolutePath());

recorder.prepare();

isRecording=true;

recorder.start();

}catch(IOExceptione){

}

newThread(newRunnable(){

@Override

publicvoidrun(){

try{

while(isRecording){

//                        android.os.Message msg = new android.os.Message();

//                        msg.what = recorder.getMaxAmplitude() * 13 / 0x7FFF;

//                        handler.sendMessage(msg);

SystemClock.sleep(100);

}

}catch(Exceptione){

// from the crash report website, found one NPE crash from

// one android 4.0.4 htc phone

// maybe handler is null for some reason

}

}

}).start();

startTime=newDate().getTime();

returnfile==null?null:file.getAbsolutePath();

}

/**

* stop the recoding

*

* @return seconds of the voice recorded

*/

publicvoiddiscardRecording(){

if(recorder!=null){

try{

recorder.stop();

recorder.release();

recorder=null;

if(file!=null&&file.exists()&&!file.isDirectory()){

file.delete();

}

}catch(IllegalStateExceptione){

}catch(RuntimeExceptione){

}

isRecording=false;

}

}

publicintgetRatio(){

if(recorder!=null){

intratio=recorder.getMaxAmplitude()/600;

returnratio;

}

return-1;

}

publicintstopRecoding(){

if(recorder!=null){

isRecording=false;

if(startTime>-1){

try{

recorder.stop();

recorder.release();

}catch(Exceptione){

e.printStackTrace();

}

recorder=null;

}

intseconds=(int)(newDate().getTime()-startTime)/1000;

if(seconds>0){

if(file==null||!file.exists()||!file.isFile()){

returnEMError.FILE_INVALID;

}

if(file.length()==0){

file.delete();

returnEMError.FILE_INVALID;

}

}

returnseconds;

}

return0;

}

protectedvoidfinalize()throwsThrowable{

super.finalize();

if(recorder!=null){

recorder.release();

}

}

privateStringgetVoiceFileName(Stringuid){

Timenow=newTime();

now.setToNow();

this.uid=uid;

returnuid+now.toString().substring(0,15)+EXTENSION;

}

publicbooleanisRecording(){

returnisRecording;

}

publicStringgetVoiceFilePath(){

returnvoiceFilePath;

}

publicStringgetVoiceTargetFilePath(){

Timenow=newTime();

now.setToNow();

returnPathUtil.getInstance().getVoicePath()+"/"+uid+now.toString().substring(0,15)+".mp3";

}

publicStringgetVoiceFileName(){

returnvoiceFileName;

}

}

EaseVoiceRecorder.java

体积这么小而且这么方便,很大的一个缺点,也是我放弃的理由,就是录下的音质不太好,总感觉被什么笼罩着在。因此就开始寻求其它的解决方式,接下来就引用到了AudioRecord。主要是因为ios也是在使用这个所以就尝试着加入它来试试。大概描述一下简单工作流程:

1.创建一个数据流。

2.构造一个AudioRecord对象,其中需要的最小录音缓存buffer大小可以通过getMinBufferSize方法得到。如果buffer容量过小,将导致对象构造的失败。

3.初始化一个buffer,该buffer大于等于AudioRecord对象用于写声音数据的buffer大小。

4.开始录音。

5.从AudioRecord中读取声音数据到初始化buffer,将buffer中数据导入数据流。

6.停止录音。

7.关闭数据流。

首先遇到的问题是录音下来的东西是不能播放的,就是PCM格式的音频文件,也就是通常遇到的raw文件。简单的来说就是裸音。如果需要播放的话,必须要给裸数据加上头文件。这样得到的就是WAV格式的音频的文件,也是是电脑上经常使用的无损音质了,但是对于这么好的音质,引来的第一大问题就是文件体积比较大简单来说录音10秒大概就有四百多KB的大小。这可能对于即时通讯的项目是不行的,先粘贴出工具类先,方便大家其它地方有用到。

importandroid.media.AudioFormat;

importandroid.media.AudioRecord;

importandroid.os.Environment;

importandroid.text.format.Time;

importandroid.util.Log;

importjava.io.File;

importjava.io.FileInputStream;

importjava.io.FileNotFoundException;

importjava.io.FileOutputStream;

importjava.io.IOException;

importjava.util.Date;

importstaticcom.lvgou.distribution.driect.AudioFileFunc.isSdcardExit;

/**

* Created by Administrator on 2017/6/12.

*/

publicclassChatAudioRecord{

// 缓冲区字节大小

privateintbufferSizeInBytes=0;

//AudioName裸音频数据文件 ,麦克风

privateStringAudioName="";

//NewAudioName可播放的音频文件

privateStringNewAudioName="";

privateAudioRecordaudioRecord;

privatebooleanisRecord=false;// 设置正在录制的状态

publicbooleanisRecording(){

returnisRecord;

}

privatestaticChatAudioRecordmInstance;

privateChatAudioRecord(){

}

publicsynchronizedstaticChatAudioRecordgetInstance(){

if(mInstance==null)

mInstance=newChatAudioRecord();

returnmInstance;

}

publicStringgetVoiceFilePath(){

/* try {

execute(new File(NewAudioName),myuid);

Log.e("lkhfkhsdfg", "--------"+NewAudioName );

return NewAudioName;

} catch (Exception e) {

e.printStackTrace();

Log.e("lkhfkhsdfg", "---------"+e );

}

return "";*/

//        writeMP3();

returnNewAudioName;

}

privateStringmyuid="";

publicintstartRecordAndFile(Stringuid){

//判断是否有外部存储设备sdcard

if(isSdcardExit()){

if(isRecord){

returnErrorCode.E_STATE_RECODING;

}else{

if(audioRecord==null)

myuid=uid;

creatAudioRecord(uid);

audioRecord.startRecording();

// 让录制状态为true

isRecord=true;

// 开启音频文件写入线程

newThread(newAudioRecordThread()).start();

startTime=newDate().getTime();

returnErrorCode.SUCCESS;

}

}else{

returnErrorCode.E_NOSDCARD;

}

}

privatelongstartTime=-4;

publicintstopRecordAndFile(){

close();

intseconds=(int)(newDate().getTime()-startTime)/1000;

returnseconds;

}

publiclonggetRecordFileSize(){

returnAudioFileFunc.getFileSize(NewAudioName);

}

privatevoidclose(){

if(audioRecord!=null){

System.out.println("stopRecord");

isRecord=false;//停止文件写入

audioRecord.stop();

audioRecord.release();//释放资源

audioRecord=null;

}

}

privateStringuid="";

staticfinalStringEXTENSION=".mp3";

privateStringgetVoiceFileName(Stringuid,Stringnamess){

Timenow=newTime();

now.setToNow();

this.uid=uid;

returnuid+now.toString().substring(0,15)+namess;

}

privatefinalstaticStringAUDIO_RAW_FILENAME="RawAudio.raw";

privatefinalstaticStringAUDIO_WAV_FILENAME="FinalAudio.wav";

publicfinalstaticStringAUDIO_AMR_FILENAME="FinalAudio.amr";

publicStringgetRawFilePath(StringuserId){

StringmAudioRawPath="";

if(isSdcardExit()){

StringfileBasePath=Environment.getExternalStorageDirectory().getAbsolutePath();

mAudioRawPath=fileBasePath+"/"+getVoiceFileName(userId,AUDIO_RAW_FILENAME);

}

returnmAudioRawPath;

}

/**

* 获取编码后的WAV格式音频文件路径

* @return

*/

publicStringgetWavFilePath(StringuserId){

StringmAudioWavPath="";

if(isSdcardExit()){

StringfileBasePath=Environment.getExternalStorageDirectory().getAbsolutePath();

mAudioWavPath=fileBasePath+"/"+getVoiceFileName(userId,AUDIO_WAV_FILENAME);

}

returnmAudioWavPath;

}

privatevoidcreatAudioRecord(StringuserId){

// 获取音频文件路径

AudioName=getRawFilePath(userId);

//        voiceFilePath= voiceFilePath = PathUtil.getInstance().getVoicePath() + "/" + voiceFileName;

//        NewAudioName = AudioFileFunc.getWavFilePath();

NewAudioName=getWavFilePath(userId);

// 获得缓冲区字节大小

bufferSizeInBytes=AudioRecord.getMinBufferSize(AudioFileFunc.AUDIO_SAMPLE_RATE,

AudioFormat.CHANNEL_IN_STEREO,AudioFormat.ENCODING_PCM_16BIT);

// 创建AudioRecord对象

audioRecord=newAudioRecord(AudioFileFunc.AUDIO_INPUT,AudioFileFunc.AUDIO_SAMPLE_RATE,

AudioFormat.CHANNEL_IN_STEREO,AudioFormat.ENCODING_PCM_16BIT,bufferSizeInBytes);

}

classAudioRecordThreadimplementsRunnable{

@Override

publicvoidrun(){

writeDateTOFile();//往文件中写入裸数据

copyWaveFile(AudioName,NewAudioName);//给裸数据加上头文件

//            writeMP3();

}

}

/**

* 这里将数据写入文件,但是并不能播放,因为AudioRecord获得的音频是原始的裸音频,

* 如果需要播放就必须加入一些格式或者编码的头信息。但是这样的好处就是你可以对音频的 裸数据进行处理,比如你要做一个爱说话的TOM

* 猫在这里就进行音频的处理,然后重新封装 所以说这样得到的音频比较容易做一些音频的处理。

*/

privatevoidwriteDateTOFile(){

// new一个byte数组用来存一些字节数据,大小为缓冲区大小

byte[]audiodata=newbyte[bufferSizeInBytes];

FileOutputStreamfos=null;

intreadsize=0;

try{

Filefile=newFile(AudioName);

if(file.exists()){

file.delete();

}

fos=newFileOutputStream(file);// 建立一个可存取字节的文件

}catch(Exceptione){

e.printStackTrace();

}

while(isRecord==true){

readsize=audioRecord.read(audiodata,0,bufferSizeInBytes);

if(AudioRecord.ERROR_INVALID_OPERATION!=readsize&&fos!=null){

try{

fos.write(audiodata);

}catch(IOExceptione){

e.printStackTrace();

}

}

}

try{

if(fos!=null)

fos.close();// 关闭写入流

}catch(IOExceptione){

e.printStackTrace();

}

}

// 这里得到可播放的音频文件

privatevoidcopyWaveFile(StringinFilename,StringoutFilename){

FileInputStreamin=null;

FileOutputStreamout=null;

longtotalAudioLen=0;

longtotalDataLen=totalAudioLen+36;

longlongSampleRate=AudioFileFunc.AUDIO_SAMPLE_RATE;

intchannels=2;

longbyteRate=16*AudioFileFunc.AUDIO_SAMPLE_RATE*channels/8;

byte[]data=newbyte[bufferSizeInBytes];

try{

in=newFileInputStream(inFilename);

out=newFileOutputStream(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();

}catch(FileNotFoundExceptione){

e.printStackTrace();

}catch(IOExceptione){

e.printStackTrace();

}

}

/**

* 这里提供一个头信息。插入这些信息就可以得到可以播放的文件。

* 为我为啥插入这44个字节,这个还真没深入研究,不过你随便打开一个wav

* 音频的文件,可以发现前面的头文件可以说基本一样哦。每种格式的文件都有

* 自己特有的头文件。

*/

privatevoidWriteWaveFileHeader(FileOutputStreamout,longtotalAudioLen,

longtotalDataLen,longlongSampleRate,intchannels,longbyteRate)

throwsIOException{

byte[]header=newbyte[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';

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';

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);

}

}

ChatAudioRecord.java

接下来就来处理体积大这个问题,首先我用到的是lame,就是去网上找到mp3lame.so库,然后修改它的mk文件,将其引入到项目中,其实也就是在录音的过程中将音频文件的编码改成mp3格式。于是就在github上找了相关的文章

https://github.com/search?q=android+mp3+&type=Repositories&ref=searchresults

https://github.com/yhirano/Mp3VoiceRecorderSampleForAndroid

关于使用,只需要项目中的libmp3lame.so文件,和com.uraroji.garage.android.lame包下的SimpleLame.java文件和RecMicToMp3.java文件,注意SimpleLame.java必须放在com.uraroji.garage.android.lame包下。

但是对于按照它的操作出现的问题就是

Android 录音MediaRecorder到AudioRecord_第1张图片

这一下就尴尬了,然后在网上找了一些解决方式,后来报错就是找不到so库的错,接着我又将so库复制到其它的几个文件夹里面还是相同的问题,我然后就去修改那些.h和.c文件。希望在这里找到一些解药,对于一个多年没接触C语言的人来说,那个里面的调用着实让我看不懂。因此就放弃了这条路。

接着就是VoAACEncorder,这个方式,我也记不得是在github上哪个项目弄下来的。首先是它的demo可以运行,并且音质也很不错,基本就是AAC的音频格式,并且这个格式的体积并不大,我录了120秒的音频文件大概就只有470kb左右的大小吧,当我看到这里的时候认定自己选择的就是这个,接下来就想方设法的研究出来然后插入到自己项目中。主要的就是jni中so库的试用,github里的项目是ec编写的,但是对于现在这个as称道的时代,还是一个一个复制吧。就两个so文件libAacEncoder.so和libVoAACEncoder.so。方法也就那几个,我也粘贴出util.

首先出现的问题就和lame后面那个一样,找不到so库,

Android 录音MediaRecorder到AudioRecord_第2张图片

这对于一个使用jni不熟练的人来说确实是个难题。其实这个问题的原因就是那个java文件中的native方法,native标识的方法就是调用C语言的地方,由于没有找到指定的位子。但是,对于标识指定位子的文件就是so库里面的mk文件里面标识的,这个对于我们来说是不方便编辑和更改的,但是我们可以更改外部编写有native方法文件的包名啊。按照mk文件里的包名进行重新创造。首先在我的一台测试机上运行成功,也能正常录音,但是当我换一台测试机的时候,又报了一个错,就是缺少64位的so库。这个问题就很尴尬了啊,提供的是armeabi里面的so啊,这个应该是32位和64位通用的啊,然后我发现我还有一些其它的文件夹,安卓在寻找so文件的时候是会优先寻找和自己匹配的文件夹下的so,没有找打就会报错。接着我就引入了armeabi-v7a包,在这个下面把armeabi里面的so文件拷贝过来,这个也就是处理兼容性问题的地方。然后就能顺利运行和录音了。但是还是存在一个缺点,对于即时通讯的项目是录音完成后马上发送的,这个录音的结构来说是边录边转码的,很有可能在你没转码成功的情况下就进行发送,这就导致服务器那边出现找不到文件的错误。我的处理方式是在录音完成后会停顿一秒,然后再进行发送,这样就大大降低了发送失败的次数。但偶尔还是会存在失败,接着我就在失败返回监听的方法里面再次将失败了的文件重新发送,如果还是失败,那么我就告诉用户失败了,在消息后面加一个红色边框里面是感叹号,让用户自己点击后重新发送,目前这样的处理方式是没有出现过失败的了。

importandroid.text.format.Time;

importandroid.util.Log;

importcom.lvgou.distribution.utils.PathUtil;

importjava.util.Date;

importlwx.linin.aac.VoAAC;

/**

* Created by Administrator on 2017/6/14.

*/

publicclassACCVoiceRecorder{

privateintsampleRateInHz=16000;

privateVoAACaac;

privateStringfileName;

staticfinalStringEXTENSION=".mp3";

privateStringuid="";

privatelongstartTime=-4;

privatebooleanisRecording=false;//是否正在录音

privateStringvoiceFilePath;

publicACCVoiceRecorder(){

}

publicStringgetVoiceFilePath(){

returnvoiceFilePath;

}

publicvoidstartRecording(StringuserId){

fileName=getVoiceFileName(userId);

voiceFilePath=PathUtil.getInstance().getVoicePath()+"/"+fileName;

Log.e("aslkdfhakshfd","------------"+voiceFilePath);

aac=newVoAAC(voiceFilePath);

aac.sampleRateInHz(sampleRateInHz);

aac.start();

startTime=newDate().getTime();

isRecording=true;

}

privateStringgetVoiceFileName(Stringuid){

Timenow=newTime();

now.setToNow();

this.uid=uid;

returnuid+now.toString().substring(0,15)+EXTENSION;

}

publicintstopRecoding(){

intseconds=(int)(newDate().getTime()-startTime)/1000;

aac.stop();

isRecording=false;

returnseconds;

}

publicvoiddiscardRecording(){

stopRecoding();

}

publicbooleanisRecording(){

returnisRecording;

}

publicStringgetVoiceFileName(){

returnfileName;

}

}


csdn项目地址:http://blog.csdn.net/greatdaocaoren/article/details/73433527

你可能感兴趣的:(Android 录音MediaRecorder到AudioRecord)