Android录音mp3格式实例详解
Android录音支持的格式有amr、aac,但这两种音频格式在跨平台上表现并不好。
MP3显然才是跨平台的最佳选择。
项目地址
GavinCT/AndroidMP3Recorder
实现思路概述
在分析代码前,我们需要明确几个问题
1.如何最终生成MP3
实现MP3格式最好是借助Lame这个成熟的解决方案。
对于Android来说,需要借助JNI来调用Lame的C语言代码,实现音频格式的转化。
2.如何获取最初的音频数据
AudioRecord类可以直接帮助我们获取音频数据。
3.如何进行转换
网上有代码是先录制后转为MP3,这种效率比较低。因为如果录音时间过长,转换时间就会相应变长,用户在存储录音时需要等待的时间就会变长。
SamsungDevelopers先录后转示例代码
显然,这种方案是不可取的。
我们需要的是边录边转的实现方式,这样在停止录音进行存储的时候,就不会花费太长时间。
实现代码介绍
既然是录音,我们上面也提到了需要使用AudioRecord类,我们就从这个类的构造器开始说起
构造器
publicAudioRecord(intaudioSource,intsampleRateInHz,intchannelConfig,intaudioFormat,intbufferSizeInBytes)
构造器参数很多,我们一点一点来看:
- audioSource:声源,一般使用MediaRecorder.AudioSource.MIC表示来自于麦克风
- sampleRateInHz:官方明确说到只有44100Hz是所有设备都支持的。其他22050、16000和11025只能在某些设备上使用。
- channelConfig:有立体声(CHANNEL_IN_STEREO)和单声道(CHANNEL_IN_MONO)两种。但只有单声道(CHANNEL_IN_MONO)是所有设备都支持的。
- audioFormat:有ENCODING_PCM_16BIT和ENCODING_PCM_8BIT两种音频编码格式。同样的,官方声明只有ENCODING_PCM_16BIT是所有设备都支持的。
- bufferSizeInBytes:录音期间声音数据的写入缓冲区大小(单位是字节)。
其实从上面的解释可以看到,类的参数很多,但为了保证在所有设备上可以使用,我们真正需要填写的只有一个参数:bufferSizeInBytes,其他都可以使用通用的参数而不用自己费心来选择。
在深究bufferSizeInBytes该传入什么之前,我们先略过这一段,先来说一下录音的读取与转换。
录音的读取与转换策略
录音的读取其实和UDP差不多,需要不断的读取数据。
既然是不断,那么我们当然需要循环读取,意味着我们需要一个线程来单独读取录音,避免阻塞主线程。
还和UDP差不多的是,如果不及时读取,数据超过缓冲区大小,会造成这段录音数据的丢失。
上面提到过,我们想要实现的是边录边转。那么问题来了,如果我们读取完数据后接着将数据传给Lame进行MP3编码,Lame的编码时间是不确定的,是不是有可能造成数据的丢失呢?
答案当然是有可能,所以我们不能巧合编程。
我们需要另外一个线程,即数据编码线程来专门进行MP3编码,而当前的录音读取线程只负责读取录音PCM数据。
有了两条线程,我们还需要确认一点,什么时候编码线程开始处理数据?
编码线程处理数据的时机
传统的方法是当线程中有数据的时候开始处理,这就需要在这个线程里面不断循环查看是否有数据需要处理,有数据就开始处理,没有数据我们可以暂时休息几毫秒(当然一直不sleep也可以,但造成的系统消耗太多)。
这种方式显然也是低效的,因为无论我们让线程休息多久都可以判定为不合理。因为我们并不知道准确的时间。
那么还有别的方法么?
显然录音这个类是知道什么时候该处理数据,什么时候可以休息。
Don'tcallme,Iwillcallyou.
是的,我们应该去看看有没有监听器,让录音来通知编码线程开始工作。
AudioRecord为我们提供了这样的方法:
publicintsetPositionNotificationPeriod(intperiodInFrames) AddedinAPIlevel3 Setstheperiodatwhichthelisteneriscalled,ifsetwithsetRecordPositionUpdateListener(OnRecordPositionUpdateListener)orsetRecordPositionUpdateListener(OnRecordPositionUpdateListener,Handler).Itispossiblefornotificationstobelostiftheperiodistoosmall.
设置通知周期。以帧为单位。
到这里,我们可以回来来解释bufferSizeInBytes大小的传入了。
缓冲区的大小
其实AudioRecord类提供了一个方便的方法getMinBufferSize来获取缓冲区的大小。
publicstaticintgetMinBufferSize(intsampleRateInHz,intchannelConfig,intaudioFormat)
这里的3个参数,其实我们都可以从构造器的参数里看到,因此传入并没有什么问题。
但关键在如上面我们设置了周期单位,如果获得的缓冲区大小不是周期单位的整数倍呢?
不是整数倍当然会如我们猜想的一样造成数据丢失,因此我们还需要一些数据的纠正来保证缓冲区大小是整数倍。
mBufferSize=AudioRecord.getMinBufferSize(DEFAULT_SAMPLING_RATE, DEFAULT_CHANNEL_CONFIG,DEFAULT_AUDIO_FORMAT.getAudioFormat()); intbytesPerFrame=DEFAULT_AUDIO_FORMAT.getBytesPerFrame(); /*Getnumberofsamples.Calculatethebuffersize *(rounduptothefactorofgivenframesize) *使能被整除,方便下面的周期性通知 **/ intframeSize=mBufferSize/bytesPerFrame; if(frameSize%FRAME_COUNT!=0){ frameSize+=(FRAME_COUNT-frameSize%FRAME_COUNT); mBufferSize=frameSize*bytesPerFrame; }
讲完了数据的获取线程和编码线程,我们来仔细看看帮助我们实现MP3编码的功臣:Lame
Lame的获取与编译
Lame在线下载地址
步骤
解压libmp3lame到jni目录.
拷贝lame.h(include目录下)
创建Android.mk
LOCAL_PATH:=$(callmy-dir) include$(CLEAR_VARS) LOCAL_MODULE:=mp3lame LOCAL_SRC_FILES:=bitstream.cfft.cid3tag.cmpglib_interface.cpresets.cquantize.creservoir.ctables.cutil.cVbrTag.cencoder.cgain_analysis.clame.cnewmdct.cpsymodel.cquantize_pvt.cset_get.ctakehiro.cvbrquantize.cversion.c include$(BUILD_SHARED_LIBRARY)
删除非.c/.h文件:GNUautotools,Makefile.amMakefile.inlibmp3lame_vc8.vcprojlogoe.icodepcomp,foldersi386等无用文件。
编辑jni/utils.h。把externieee754_float32_tfast_log2(ieee754_float32_tx);替换为externfloatfast_log2(floatx);。如果忘了替换,编译时会报出以下错误:
[armeabi]Compilethumb:mp3lame<=bitstream.c Infileincludedfromjni/bitstream.c:36:0: jni/util.h:574:5:error:unknowntypename'ieee754_float32_t' jni/util.h:574:40:error:unknowntypename'ieee754_float32_t' make.exe:***[obj/local/armeabi/objs/mp3lame/bitstream.o]Error1
编译库文件。可能会报出警告,忽略即可。
Lame需要对外提供的方法
- init初始化
- inSamplerate:输入采样频率Hz
- inChannel:输入声道数
- outSamplerate:输出采样频率Hz
- outBitrate:Encodedbitrate.KHz
- quality:MP3音频质量。0~9。其中0是最好,非常慢,9是最差。
推荐:
2:near-bestquality,nottooslow
5:goodquality,fast
7:okquality,reallyfast
privatestaticfinalintDEFAULT_LAME_MP3_QUALITY=7; /** *与DEFAULT_CHANNEL_CONFIG相关,因为是mono单声,所以是1 */ privatestaticfinalintDEFAULT_LAME_IN_CHANNEL=1; /** *Encodedbitrate.MP3filewillbeencodedwithbitrate32kbps */ privatestaticfinalintDEFAULT_LAME_MP3_BIT_RATE=32; /* *Initializelamebuffer *mp3samplingrateisthesameastherecordedpcmsamplingrate *Thebitrateis32kbps * */ LameUtil.init(DEFAULT_SAMPLING_RATE,DEFAULT_LAME_IN_CHANNEL,DEFAULT_SAMPLING_RATE,DEFAULT_LAME_MP3_BIT_RATE,DEFAULT_LAME_MP3_QUALITY);
encode
- bufferLeft:左声道数据
- bufferRight:右声道数据
- samples:每个声道输入数据大小
- mp3buf:用于接收转换后的数据。7200+(1.25*buffer_l.length)
这里需要解释一下:
Tasktask=mTasks.remove(0); short[]buffer=task.getData(); intreadSize=task.getReadSize(); intencodedSize=LameUtil.encode(buffer,buffer,readSize,mMp3Buffer);
- 左右声道:当前声道选的是单声道,因此两边传入一样的buffer。
- 输入数据大小:录音线程读取到buffer中的数据不一定是占满的,所以read方法会返回当前大小size,即前size个数据是有效的音频数据,后面的数据是以前留下的废数据。这个size同样需要传入到Lame编码器中用于编码。
- mp3的buffer:官方规定了计算公式:7200+(1.25*buffer_l.length)。(可以在lame.h文件中看到)
flush
将MP3结尾信息写入buffer中。
传入参数:mp3buf至少7200字节。这里还是用以前定义的mp3buf来传入,避免创建过多的数组。
close
关闭释放Lame
OK,到这里,核心的转换代码就完成了,我们再来点锦上添花的东西。
音量
一般我们在做录音的时候,都会有一个需求,根据音量的大小显示一个动画,让录音显得更生动一些。
当然,我在这个库里也提供了。
那么怎么来计算音量呢?
我参考了三星的音量计算。
总结如下:
/** *此计算方法来自samsung开发范例 * *@parambuffer *@paramreadSize */ privatevoidcalculateRealVolume(short[]buffer,intreadSize){ intsum=0; for(inti=0;i0){ doubleamplitude=sum/readSize; mVolume=(int)Math.sqrt(amplitude); } };
关于最大音量
其实对于音量,我不是特别明白。
最大音量在三星的代码中给出的是4000,但是我在实际的测试中发现,这个计算公式得出的音量大小一般都在1500以内。
因此在我提供的录音库里面,我把最大音量规定为了2000。
这块儿欢迎大家来提宝贵意见。
MP3录音实现参考
yhirano/Mp3VoiceRecorderSampleForAndroid
日本人写的,感觉他的判断不完善,有点巧合编程的意思,也或许是我没看懂。
talzeus/AndroidMp3Recorder
比较严谨的代码。主要依据这个库进行的修改。
存在的问题:
AudioRecord传入参数很多没有按Android规定传入。如采样频率使用了22050Hz。
使用了自己构造的RingBuffer,看这有点头晕。我在库里使用List来存储未编码的音频数据,更容易理解。
没有提供音量大小。