分享

Android MP3录音实现

 苦修茶馆 2016-02-17

Android录音支持的格式有amr、aac,但这两种音频格式在跨平台上表现并不好。
MP3显然才是跨平台的最佳选择。
近期由于项目需要,实现了这个需求,代码托管在Github上,欢迎拍砖

项目地址

GavinCT/AndroidMP3Recorder

使用方法见:README.md

推荐直接下载libs.zip集成到自己项目里。

【温馨提示:只下载此zip可以使用Chrome插件GitHub Mate

实现思路概述

在分析代码前,我们需要明确几个问题

1. 如何最终生成MP3

实现MP3格式最好是借助Lame这个成熟的解决方案。
对于Android来说,需要借助JNI来调用Lame的C语言代码,实现音频格式的转化。

2. 如何获取最初的音频数据

AudioRecord类可以直接帮助我们获取音频数据。

3. 如何进行转换

网上有代码是先录制后转为MP3,这种效率比较低。因为如果录音时间过长,转换时间就会相应变长,用户在存储录音时需要等待的时间就会变长。
Samsung Developers先录后转示例代码
显然,这种方案是不可取的。
我们需要的是边录边转的实现方式,这样在停止录音进行存储的时候,就不会花费太长时间。

实现代码介绍

既然是录音,我们上面也提到了需要使用AudioRecord类,我们就从这个类的构造器开始说起

构造器

  1. public AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)  

构造器参数很多,我们一点一点来看:
  • 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't call me , I will call you.
是的,我们应该去看看有没有监听器,让录音来通知编码线程开始工作。
AudioRecord为我们提供了这样的方法:

  1. public int setPositionNotificationPeriod (int periodInFrames)  
  2.   
  3. Added in API level 3  
  4. Sets the period at which the listener is called, if set with setRecordPositionUpdateListener(OnRecordPositionUpdateListener) or setRecordPositionUpdateListener(OnRecordPositionUpdateListener, Handler). It is possible for notifications to be lost if the period is too small.  

设置通知周期。 以帧为单位。
到这里,我们可以回来来解释bufferSizeInBytes大小的传入了。

缓冲区的大小

其实AudioRecord类提供了一个方便的方法getMinBufferSize来获取缓冲区的大小。

  1. public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)  

这里的3个参数,其实我们都可以从构造器的参数里看到,因此传入并没有什么问题。但关键在如上面我们设置了周期单位,如果获得的缓冲区大小不是周期单位的整数倍呢?不是整数倍当然会如我们猜想的一样造成数据丢失,因此我们还需要一些数据的纠正来保证缓冲区大小是整数倍。
  1. mBufferSize = AudioRecord.getMinBufferSize(DEFAULT_SAMPLING_RATE,  
  2.         DEFAULT_CHANNEL_CONFIG, DEFAULT_AUDIO_FORMAT.getAudioFormat());  
  3.   
  4. int bytesPerFrame = DEFAULT_AUDIO_FORMAT.getBytesPerFrame();  
  5. /* Get number of samples. Calculate the buffer size  
  6.  * (round up to the factor of given frame size)  
  7.  * 使能被整除,方便下面的周期性通知 
  8.  * */  
  9. int frameSize = mBufferSize / bytesPerFrame;  
  10. if (frameSize % FRAME_COUNT != 0) {  
  11.     frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT);  
  12.     mBufferSize = frameSize * bytesPerFrame;  
  13. }  

讲完了数据的获取线程和编码线程,我们来仔细看看帮助我们实现MP3编码的功臣:Lame

Lame的获取与编译

Lame在线下载地址

步骤

  1. 解压libmp3lame 到jni目录.
  2. 拷贝 lame.h (include目录下)
  3. 创建Android.mk

    1. LOCAL_PATH := $(call my-dir)  
    2. include $(CLEAR_VARS)  
    3. LOCAL_MODULE    := mp3lame  
    4. LOCAL_SRC_FILES := bitstream.c fft.c id3tag.c mpglib_interface.c presets.c  quantize.c   reservoir.c tables.c  util.c  VbrTag.c encoder.c  gain_analysis.c lame.c  newmdct.c   psymodel.c quantize_pvt.c set_get.c  takehiro.c vbrquantize.c version.c  
    5. include $(BUILD_SHARED_LIBRARY)  

     

    删除非.c/.h文件:GNU autotools, Makefile.am Makefile.in libmp3lame_vc8.vcproj logoe.ico depcomp, folders i386 等无用文件。
    
  4. 编辑 jni/utils.h。把extern ieee754_float32_t fast_log2(ieee754_float32_t x);替换为extern float fast_log2(float x);。如果忘了替换,编译时会报出以下错误:

    1. [armeabi] Compile thumb  : mp3lame <= bitstream.c  
    2. In file included from jni/bitstream.c:36:0:  
    3. jni/util.h:574:5: error: unknown type name 'ieee754_float32_t'  
    4. jni/util.h:574:40: error: unknown type name 'ieee754_float32_t'  
    5. make.exe: *** [obj/local/armeabi/objs/mp3lame/bitstream.o] Error 1  
  5. 编译库文件。可能会报出警告,忽略即可。

Lame需要对外提供的方法

init 初始化

  • inSamplerate : 输入采样频率 Hz
  • inChannel : 输入声道数
  • outSamplerate : 输出采样频率 Hz
  • outBitrate : Encoded bit rate. KHz
  • quality : MP3音频质量。0~9。 其中0是最好,非常慢,9是最差。
    推荐:
    2 :near-best quality, not too slow
    5 :good quality, fast
    7 :ok quality, really fast
  1. private static final int DEFAULT_LAME_MP3_QUALITY = 7;  
  2. /**  
  3.  * 与DEFAULT_CHANNEL_CONFIG相关,因为是mono单声,所以是1  
  4.  */  
  5. private static final int DEFAULT_LAME_IN_CHANNEL = 1;  
  6. /**  
  7.  *  Encoded bit rate. MP3 file will be encoded with bit rate 32kbps   
  8.  */   
  9. private static final int DEFAULT_LAME_MP3_BIT_RATE = 32;  
  10.   
  11.   
  12.   
  13. /*  
  14. * Initialize lame buffer  
  15. * mp3 sampling rate is the same as the recorded pcm sampling rate   
  16. * The bit rate is 32kbps  
  17. *   
  18. */  
  19. 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)
    这里需要解释一下:
  1. Task task = mTasks.remove(0);  
  2. short[] buffer = task.getData();  
  3. int readSize = task.getReadSize();  
  4. int encodedSize = 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,到这里,核心的转换代码就完成了,我们再来点锦上添花的东西。

音量

一般我们在做录音的时候,都会有一个需求,根据音量的大小显示一个动画,让录音显得更生动一些。
当然,我在这个库里也提供了。
那么怎么来计算音量呢?
我参考了三星的音量计算
总结如下:

  1. /**  
  2. * 此计算方法来自samsung开发范例  
  3. *   
  4. * @param buffer  
  5. * @param readSize  
  6. */  
  7. private void calculateRealVolume(short[] buffer, int readSize) {  
  8.     int sum = 0;  
  9.     for (int i = 0; i < readSize; i++) {    
  10.         sum += buffer[i] * buffer[i];   
  11.     }   
  12.     if (readSize > 0) {  
  13.         double amplitude = sum / readSize;  
  14.         mVolume = (int) Math.sqrt(amplitude);  
  15.     }  
  16. };  

关于最大音量

其实对于音量,我不是特别明白。
最大音量在三星的代码中给出的是4000,但是我在实际的测试中发现,这个计算公式得出的音量大小一般都在1500以内。
因此在我提供的录音库里面,我把最大音量规定为了2000。
这块儿欢迎大家来提宝贵意见。

MP3录音实现参考

  • yhirano/Mp3VoiceRecorderSampleForAndroid
    日本人写的,感觉他的判断不完善,有点巧合编程的意思,也或许是我没看懂。
  • talzeus/AndroidMp3Recorder
    比较严谨的代码。主要依据这个库进行的修改。
    存在的问题:

    • AudioRecord传入参数很多没有按Android规定传入。如采样频率使用了22050Hz。
    • 使用了自己构造的RingBuffer,看这有点头晕。 我在库里使用List来存储未编码的音频数据,更容易理解。
    • 没有提供音量大小。


==========================================================================================

欢迎加入我们的技术交流群:
Android群: 66756039
JavaEE群:  361579846 


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多