这几天,接了一个政府的项目,其中有一个需求是可以在Android本地录音并且传送到服务器,让服务器的WEB端可以进行播放。然后我使用的是Android自己的MediaRecorder,但是这个录音生成的编码形式非常具有局限性,其生成的格式也非常有限,主要有如下几种

public final class AudioEncoder {
        public static final int AAC = 3;
        public static final int AAC_ELD = 5;
        public static final int AMR_NB = 1;
        public static final int AMR_WB = 2;
        public static final int DEFAULT = 0;
        public static final int HE_AAC = 4;
        public static final int VORBIS = 6;

        AudioEncoder() {
            throw new RuntimeException("Stub!");
        }
    }

好像也就三种…然后我们的后台就来找我,说咱们录制的这个AMR给他,后台使用不了,后来我想了一下为了保证以后通用和拓展性,我决定,还是把问题留给自己吧。那么就在本地生成MP3文件之后,在给后台吧。
Android录音支持的格式有amr、aac。但是这两种音频格式在跨平台上表现并不好。是非常差!!!

那么我们开始今天的第一个话题
一、MediaRecorder和AudioRecord的区别和联系
MediaRecorder和AudioRecord都可以录制音频,区别是MediaRecorder录制的音频文件是经过压缩后的,需要设置编码器*。并且录制的音频文件可以用系统自带的Music播放器播放。*
而AudioRecord录制的是PCM格式的音频文件,需要用AudioTrack来播放,AudioTrack更接近底层。
在用MediaRecorder进行录制音视频时,最终还是会创建AudioRecord用来与AudioFlinger进行交互。
C++层MediaRecorder创建AudioRecord类的代码位于AudioSource类构造函数中,代码如下:

mRecord = new AudioRecord(  
            inputSource, sampleRate, AudioSystem::PCM_16_BIT,  
            channels > 1? AudioSystem::CHANNEL_IN_STEREO: AudioSystem::CHANNEL_IN_MONO,  
            16 * kMaxBufferSize / sizeof(int16_t), /* Enable ping-pong buffers */  
            flags);

二、那么由于MediaRecorder的局限性我在实现了第一版之后最后还是要回到AudioRecorder上来。所以重点来了,介绍AudioRecorder的使用,以及最后如何生成MP3。

构造器

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)

设置通知周期。 以帧为单位。
到这里,我们可以回来来解释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