Android用AudioRecord+MediaCodec采集音频和音频编码 & 音频一些基本概念

 相关笔记:Android MediaCodec简单总结_丞恤猿的博客-CSDN博客

#.音频的一些相关概念简介

0.整体介绍
    声波是一种机械波,我们听到声音,是因为耳朵鼓膜接收到了声波,然后听觉神经做了相应处理。
    机械波有震动频率和震动幅度,对于频率和振幅恒定的声波,若以时间为x值,声波振幅为y值,则在坐标系中绘制出的会类似一条正弦函数曲线。不过现实中都是多个声源进行各种复杂的震动,最终的震动波形是很多简单正弦波形相叠加的结果。
    计算机中只能存储离散的数据,所能做的只是周期性的采样声波震动数据,然后播放时尽可能地还原出原来的声波效果。
1.音频采样率
每秒采集的声波样本数(网上查资料,44100应该是当前唯一能保证在所有Android设备上工作的采样率)
   
2.采样精度(位宽)
每个样本占用多少bit
显然,采样率和采样精度越高,越能还原出原始声波效果,不过占用的空间也更多。
3.声道
每个声道中都可以存储一组从声源采样来的音频波形数据。
Android中采集音频时支持单声道(AudioFormat.CHANNEL_IN_MONO)和双声道立体声(AudioFormat.CHANNEL_IN_STEREO)
相关解释说明:
   现实中物体的震动肯定不是单点震动,而是很很多方位的多个位置无数个点一起震动,
        每个点都是一个音源,有自己的声音波形,无数波形重合在一起,是我们最终听到效果。
        如果声音的采集、存储、播放能完美还原这一过程,那无疑我听到的是"完美立体"的声音,但显然是不可能做到的。
    若声音采集时,真实在音源附近不同的两个位置采集两组波形,或者是模拟生成两组类似效果的波形,
        在播放时,让两个喇叭或者左右耳机来分别播放这两组不同波形,会有声音更立体的感觉。(与单声道相比)
    但两个声道的波形,在合成视频时需要两条音轨,而某些场景下,例如使用MediaMuxer合成视频时,MediaMuxer最多只能添加一条音轨。此时,要先把多个声道的数据合成一条声道再使用。
##.几种常见的音频格式
PCM:  原始采集的音频数据,未经压缩。
WAV:数据本身的格式为PCM或压缩型,属于无损格式,只是文件存储后缀名为.wav。
MP3 :  采用MP3音频压缩技术,压缩比4:1~10:1之间。
AAC:Advanced Audio Coding, AAC压缩比通常为18:1。相比MP3,采用更高效的编码算法,音质更佳,且文件更小。

#.AudioRecord

   Android内部提供的音频采集功能封装类,其真正的音频采集功能是在native底层服务中实现的。
   AudioRecord有一个对应的音频采集缓冲区,采集过程中会源源不断地把采集到的音频数据放到该缓冲区上,而通过AudioRecord的read(xxx)方法可以从该缓冲区上取出音频数据,做自己需要的业务处理。取出音频数据一定要及时,否则音频采集缓冲区没有足够空间存放新的音频采集数据,会出现数据溢出错误。
##.主要API
1.startRecording():开始音频采集
2.int read(ByteBuffer audioBuffer, int sizeInBytes,int readMode)
第一个参数是供取出的数据存放的Buffer;
第二个参数是请求读取的数据大小;
第三个标志位,用于设置是否阻塞线程,
   阻塞时,会直到AudioRecord音频缓冲区中存储够了要读取的音频数据,才返回
   不阻塞时,会立即从缓存区中读取尽可能多的数据(不会超过请求的数据量)
   返回值为实际读取出的字节数
注意:
    AudioRecord缓冲区上数据上不断有新的采集数据放入,若不及时取走这些数据,有可能产生数据溢出错误
    要想完全避免AudioRecord缓冲区上的数据溢出错误,最好的方法是新建一个子线程来专门做声音采集并从缓冲区上取数据,取出的数据放在一个队列中,每次的数据都记录放入的时间戳,而编码线程从队列中取数据,对于已经过去很久认为已过时数据就直接丢弃掉。
3.stop():停止音频采集
4.release():释放实例和资源
#.MediaCodec音频编码
与进行视频编码时类似,但要注意:
与视频编码时最后一帧通过mEncoder.signalEndOfInputStream()发送不同,
音频帧的END-OF-STREAM标识位要在queueInputBuffer()时传入
编码器的输入帧/输出帧的时间戳单位都为微秒,us
int flag = endOfStream ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0;
mEncoder.queueInputBuffer(bufferIndex, startOffset, audioSize, mPts, flag);
##.处理输入缓冲区的代码示例和分析(见注释):
//获取一个可用输入缓冲区的索引,若返回索引>=0表示获取到了可用的输入缓冲区
//第二个参数timeoutUs为等待时长,线程会在此阻塞,一直等到有有效的索引值返回或超时
//若timeoutUs<0,表示无限期等待;若timeoutUs=0,会立马返回,当然不一定是返回有效的索引
int bufferIndex = mEncoder.dequeueInputBuffer(0);
if (bufferIndex >= 0) {
    ByteBuffer[] inputBuffers = mEncoder.getInputBuffers();
    ByteBuffer bufferCache = inputBuffers[bufferIndex];
    bufferCache.clear();
    int startOffset = bufferCache.position();
    int requestSize = Math.min(mAudioConfig.samplePerFrame, bufferCache.remaining());
    //1.第二个参数是请求读取的数据大小
    // 此处第三个标志位,用于设置是否阻塞线程,
    //  阻塞时,会直到AudioRecord音频缓冲区中存储够了要读取的音频数据,才返回
    //  不阻塞时,会立即从缓存区中读取尽可能多的数据(不会超过请求的数据量)
    //返回值为实际读取出的字节数
    //2.AudioRecord缓冲区上数据上不断有新的采集数据放入,若不及时取走这些数据,有可能产生数据溢出错误
    //3.要想完全避免AudioRecord缓冲区上的数据溢出错误,最好的方法是新建一个子线程来专门做声音采集并从缓冲区上取数据,
    //  取出的数据放在一个队列中,每次的数据都记录放入的时间戳,而编码线程从队列中取数据,对于已经过去很久的数据就直接丢弃掉
    int readMode =  endOfStream ? AudioRecord.READ_NON_BLOCKING : AudioRecord.READ_BLOCKING;
    int audioSize = record.read(bufferCache, requestSize, readMode);
    if (audioSize == AudioRecord.ERROR_INVALID_OPERATION
            || audioSize == AudioRecord.ERROR_BAD_VALUE) {
        //读取数据失败
        //.............进行相应处理..............
    } else if(audioSize == 0){
        //读取数据长度为0
        //.............进行相应处理..............
    } else {
        int endOffset = startOffset + audioSize - 1;
       
        //.............省略..............
       
        //将输入缓冲区放回队列,供MediaCodec去取出编码
        //与视频编码时最后一帧通过mEncoder.signalEndOfInputStream()发送不同,
        //音频帧的END-OF-STREAM标识位要在queueInputBuffer()时传入
        //编码器的输入帧/输出帧的时间戳单位都为微秒,us
        int flag = endOfStream ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0;
        mEncoder.queueInputBuffer(bufferIndex, startOffset, audioSize, mPts, flag);

        //.............省略..............
    }
}
##.处理输出缓冲区数据的代码示例和分析(见注释):
            //缓冲区信息,可从通过该info获取输出帧的PTS、数据大小,或判断是否为最后一帧等
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            //获取一个可用输出缓冲区的索引,若返回-1表示当前无可用的输出缓冲区
            //第二个参数timeoutUs为等待时长,线程会在此阻塞,一直等到有有效的索引值返回或超时
            //若timeoutUs<0,表示无限期等待;若timeoutUs=0,会立马返回,当然不一定是返回有效的索引
            int bufferIndex = mEncoder.dequeueOutputBuffer(info, 0);
            //获取输出缓冲区队列,其实就是个ByteBuffer数组
            ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers();
            if(bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                //输出格式已经更改,后继会按照新的输出格式来输出
                //正常情况下,编码开始后,首次获取到第一个有效输出bufferIndex(非-1)会是此值
                //在此时可以获取到编码器输出格式,并做相应处理
                MediaFormat mediaFormat = mEncoder.getOutputFormat();
                //...........做相应处理.............
            } else if (bufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                //输出缓冲区已更改,需要获取新的输出缓冲区
                outputBuffers = mEncoder.getOutputBuffers();
            } else if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                //bufferIndex=-1,表示当前无有效输出缓冲区
                //...........做相应处理.............
            } else if (bufferIndex < 0) {
                //其它bufferIndex<0的情况
                //...........做相应处理.............
            } else if (bufferIndex >= 0) {
                //返回了有效的输出缓冲区
                ByteBuffer data = outputBuffers[bufferIndex];
                //输出缓冲区内有数据
                if(data != null && info.size > 0){
                    //根据输出数据的偏移位置和大小,设置ByteBuffer上的可读写范围
                    data.position(info.offset);
                    data.limit(info.offset + info.size);
                    //...........做相应处理.............
                }
                //释放输出缓冲区
                mEncoder.releaseOutputBuffer(bufferIndex, false);
                //到达最后一个数据输出帧
                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    //...........做相应处理.............
                    break;
                }
            }

猜你喜欢

转载自blog.csdn.net/u013914309/article/details/124720123