研究录音是源于即时通讯的项目。写出一个即时通讯很简单,但是写好一个即时通讯就不是一件容易的事,比如聊天中语音的加入。接下来就来描述一下自己对语音的见解和处理方式。
首先写到语音,当然首当其冲的是运用到网上百分之八九十的处理方案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包下。
但是对于按照它的操作出现的问题就是
这一下就尴尬了,然后在网上找了一些解决方式,后来报错就是找不到so库的错,接着我又将so库复制到其它的几个文件夹里面还是相同的问题,我然后就去修改那些.h和.c文件。希望在这里找到一些解药,对于一个多年没接触C语言的人来说,那个里面的调用着实让我看不懂。因此就放弃了这条路。
接着就是VoAACEncorder,这个方式,我也记不得是在github上哪个项目弄下来的。首先是它的demo可以运行,并且音质也很不错,基本就是AAC的音频格式,并且这个格式的体积并不大,我录了120秒的音频文件大概就只有470kb左右的大小吧,当我看到这里的时候认定自己选择的就是这个,接下来就想方设法的研究出来然后插入到自己项目中。主要的就是jni中so库的试用,github里的项目是ec编写的,但是对于现在这个as称道的时代,还是一个一个复制吧。就两个so文件libAacEncoder.so和libVoAACEncoder.so。方法也就那几个,我也粘贴出util.
首先出现的问题就和lame后面那个一样,找不到so库,
这对于一个使用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