Android录音支持的格式有amr、aac,但这两种音频格式在跨平台上表现并不好。
MP3显然才是跨平台的最佳选择。
项目地址
GavinCT/AndroidMP3Recorder
实现思路概述
在分析代码前,我们需要明确几个问题
1. 如何最终生成MP3
实现MP3格式最好是借助Lame这个成熟的解决方案。
对于Android来说,需要借助JNI来调用Lame的C语言代码,实现音频格式的转化。
2. 如何获取最初的音频数据
AudioRecord类可以直接帮助我们获取音频数据。
3. 如何进行转换
网上有代码是先录制后转为MP3,这种效率比较低。因为如果录音时间过长,转换时间就会相应变长,用户在存储录音时需要等待的时间就会变长。
Samsung Developers先录后转示例代码
显然,这种方案是不可取的。
我们需要的是边录边转的实现方式,这样在停止录音进行存储的时候,就不会花费太长时间。
实现代码介绍
既然是录音,我们上面也提到了需要使用AudioRecord类,我们就从这个类的构造器开始说起
构造器
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为我们提供了这样的方法:
public int setPositionNotificationPeriod (int periodInFrames) Added in API level 3 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来获取缓冲区的大小。
public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)
这里的3个参数,其实我们都可以从构造器的参数里看到,因此传入并没有什么问题。
但关键在如上面我们设置了周期单位,如果获得的缓冲区大小不是周期单位的整数倍呢?
不是整数倍当然会如我们猜想的一样造成数据丢失,因此我们还需要一些数据的纠正来保证缓冲区大小是整数倍。
mBufferSize = AudioRecord.getMinBufferSize(DEFAULT_SAMPLING_RATE, DEFAULT_CHANNEL_CONFIG, DEFAULT_AUDIO_FORMAT.getAudioFormat()); int bytesPerFrame = DEFAULT_AUDIO_FORMAT.getBytesPerFrame(); /* Get number of samples. Calculate the buffer size * (round up to the factor of given frame size) * 使能被整除,方便下面的周期性通知 * */ int frameSize = 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 := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := mp3lame 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 include $(BUILD_SHARED_LIBRARY)
删除非.c/.h文件:GNU autotools, Makefile.am Makefile.in libmp3lame_vc8.vcproj logoe.ico depcomp, folders i386 等无用文件。
编辑 jni/utils.h。把extern ieee754_float32_t fast_log2(ieee754_float32_t x);
替换为extern float fast_log2(float x);
。如果忘了替换,编译时会报出以下错误:
[armeabi] Compile thumb : mp3lame <= bitstream.c In file included from jni/bitstream.c:36:0: jni/util.h:574:5: error: unknown type name 'ieee754_float32_t' jni/util.h:574:40: error: unknown type name 'ieee754_float32_t' make.exe: *** [obj/local/armeabi/objs/mp3lame/bitstream.o] Error 1
编译库文件。可能会报出警告,忽略即可。
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
private static final int DEFAULT_LAME_MP3_QUALITY = 7; /** * 与DEFAULT_CHANNEL_CONFIG相关,因为是mono单声,所以是1 */ private static final int DEFAULT_LAME_IN_CHANNEL = 1; /** * Encoded bit rate. MP3 file will be encoded with bit rate 32kbps */ private static final int DEFAULT_LAME_MP3_BIT_RATE = 32; /* * Initialize lame buffer * mp3 sampling rate is the same as the recorded pcm sampling rate * The bit rate is 32kbps * */ 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)
这里需要解释一下:
Task task = mTasks.remove(0); short[] buffer = task.getData(); int readSize = task.getReadSize(); 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,到这里,核心的转换代码就完成了,我们再来点锦上添花的东西。
音量
一般我们在做录音的时候,都会有一个需求,根据音量的大小显示一个动画,让录音显得更生动一些。
当然,我在这个库里也提供了。
那么怎么来计算音量呢?
我参考了三星的音量计算。
总结如下:
/** * 此计算方法来自samsung开发范例 * * @param buffer * @param readSize */ private void calculateRealVolume(short[] buffer, int readSize) { int sum = 0; for (int i = 0; i < readSize; i++) { sum += buffer[i] * buffer[i]; } if (readSize > 0) { double amplitude = sum / readSize; mVolume = (int) Math.sqrt(amplitude); } };
关于最大音量
其实对于音量,我不是特别明白。
最大音量在三星的代码中给出的是4000,但是我在实际的测试中发现,这个计算公式得出的音量大小一般都在1500以内。
因此在我提供的录音库里面,我把最大音量规定为了2000。
这块儿欢迎大家来提宝贵意见。
MP3录音实现参考
yhirano/Mp3VoiceRecorderSampleForAndroid
日本人写的,感觉他的判断不完善,有点巧合编程的意思,也或许是我没看懂。
talzeus/AndroidMp3Recorder
比较严谨的代码。主要依据这个库进行的修改。
存在的问题:
AudioRecord传入参数很多没有按Android规定传入。如采样频率使用了22050Hz。
使用了自己构造的RingBuffer,看这有点头晕。 我在库里使用List来存储未编码的音频数据,更容易理解。
没有提供音量大小。