Android录音支持的格式有amr、aac、3gp,但这三种音频格式在跨平台上表现并不好。而mp3格式是跨平台最好的音频格式,所以如果能转成mp3格式的音频文件,那是极好的。
那转成mp3格式又有两种方式:
一、录音完毕再转,再将amr、aac、3gp三种音频文件转成mp3格式的文件。
二、边录边转,使用libmp3lame直接转为mp3格式。
这里先主要介绍第二种,即 使用lame库实现边录边转mp3的方式。这样在录音完毕时,也就转完了,效率比较高。

AudioRecord

录音需要使用到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 : 录音期间声音数据的写入缓冲区大小(单位是字节)。
这里参数audioSource 、sampleRateInHz 、channelConfig 、audioFormat 都是可以根据需要进行选择,只有bufferSizeInBytes 这个参数,需要通过计算来获得。在介绍计算方法之前,先看一下音频数据的读取与转换。

音频数据的读取与转换

录音过程中需要不断的读取数据。既然是不断,那么我们当然需要循环读取,意味着我们需要一个线程来单独读取录音,避免阻塞主线程。如果不及时读取,数据超过缓冲区大小,会造成这段录音数据的丢失。
上面提到过,我们想要实现的是边录边转。那么问题来了,如果我们读取完数据后接着将数据传给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在线下载地址
步骤
1、解压libmp3lame 到jni目录.
2、拷贝 lame.h (include目录下)。
3、创建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)

4、删除非.c/.h文件:GNU autotools, Makefile.am Makefile.in libmp3lame_vc8.vcproj logoe.ico depcomp, folders i386 等无用文件。
5、编辑 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

//=======================AudioRecord Default Settings=======================
    private static final int DEFAULT_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    /**
     * 以下三项为默认配置参数。Google Android文档明确表明只有以下3个参数是可以在所有设备上保证支持的。
     */
    private static final int DEFAULT_SAMPLING_RATE = 44100;//模拟器仅支持从麦克风输入8kHz采样率
    private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    //立体声的选择
    private static final int STEREO_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
    /**
     * 下面是对此的封装
     * private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
     */
    private static final PCMFormat DEFAULT_AUDIO_FORMAT = PCMFormat.PCM_16BIT;

    //======================Lame Default Settings=====================
    private static final int DEFAULT_LAME_MP3_QUALITY = 7;
    /**
     * 与DEFAULT_CHANNEL_CONFIG相关,因为是mono单声,所以是1
     */
    //private static final int DEFAULT_LAME_IN_CHANNEL = 1;
    /**
     * STEREO_CHANNEL_CONFIG左声道和右声道,实现立体声的选择
     */
    private static final int STEREO_LAME_IN_CHANNEL = 2;
    /**
     *  Encoded bit rate. MP3 file will be encoded with bit rate 32kbps 
     */ 
    private static final int DEFAULT_LAME_MP3_BIT_RATE = 32;

LameUtil.init(DEFAULT_SAMPLING_RATE, STEREO_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);
    }
};

现有的一个项目:AndroidMP3Recorder