MediaCodec进行AAC编解码(AudioRecord采集录音)

最近工作比较忙,很久没有更新这个系列的文章。我们先回顾一下上一篇MediaCodec进行AAC编解码(文件格式转换)的内容,里面介绍了MediaExtractor的使用,MediaCodec进行音频文件的解码和编码,ADTS的介绍和封装。今天这篇文章在此基础上跟大家一起学习如何通过Android设备进行音频的采集,然后使用MediaCodec进行AAC编码,最后输出到文件。这部分我们关注的重点就是在如何进行音频的采集。 项目代码github对应的代码版本v1.7。大家一定要注意下载对应的代码版本调试。

音频的采集涉及一个类AudioRecord。我们先介绍下这个类

AudioRecord

1.png

我们还是先看下官方的说明。AudioRecord类在Java应用程序中管理音频资源,用来记录从平台音频输入设备产生的数据。通过AudioRecord对象来完成"pulling"(读取)数据。 应用通过以下几个方法负责立即从AudioRecord对象读取:read(byte[], int, int),read(short[], int, int)或read(ByteBuffer, int).无论使用哪种音频格式,使用AudioRecord是最方便的。 在创建AudioRecord对象时,AudioRecord会初始化,并和音频缓冲区连接,用来缓冲新的音频数据。根据构造时指定的缓冲区大小,来决定AudioRecord能够记录多长的数据。从硬件设备读取的数据,应小于整个记录缓冲区。 AudioRecord的使用我们分一下几个步骤:

第一步 创建AudioRecord

AudioRecord直接使用new来创建,我们看一下构造方法:

    //---------------------------------------------------------
    // Constructor, Finalize
    //--------------------
    /**
     * Class constructor.
     * Though some invalid parameters will result in an {@link IllegalArgumentException} exception,
     * other errors do not.  Thus you should call {@link #getState()} immediately after construction
     * to confirm that the object is usable.
     * @param audioSource the recording source.
     *   See {@link MediaRecorder.AudioSource} for the recording source definitions.
     * @param sampleRateInHz the sample rate expressed in Hertz. 44100Hz is currently the only
     *   rate that is guaranteed to work on all devices, but other rates such as 22050,
     *   16000, and 11025 may work on some devices.
     *   {@link AudioFormat#SAMPLE_RATE_UNSPECIFIED} means to use a route-dependent value
     *   which is usually the sample rate of the source.
     *   {@link #getSampleRate()} can be used to retrieve the actual sample rate chosen.
     * @param channelConfig describes the configuration of the audio channels.
     *   See {@link AudioFormat#CHANNEL_IN_MONO} and
     *   {@link AudioFormat#CHANNEL_IN_STEREO}.  {@link AudioFormat#CHANNEL_IN_MONO} is guaranteed
     *   to work on all devices.
     * @param audioFormat the format in which the audio data is to be returned.
     *   See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT},
     *   and {@link AudioFormat#ENCODING_PCM_FLOAT}.
     * @param bufferSizeInBytes the total size (in bytes) of the buffer where audio data is written
     *   to during the recording. New audio data can be read from this buffer in smaller chunks
     *   than this size. See {@link #getMinBufferSize(int, int, int)} to determine the minimum
     *   required buffer size for the successful creation of an AudioRecord instance. Using values
     *   smaller than getMinBufferSize() will result in an initialization failure.
     * @throws java.lang.IllegalArgumentException
     */
    public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
            int bufferSizeInBytes)
    throws IllegalArgumentException {
        this((new AudioAttributes.Builder())
                    .setInternalCapturePreset(audioSource)
                    .build(),
                (new AudioFormat.Builder())
                    .setChannelMask(getChannelMaskFromLegacyConfig(channelConfig,
                                        true/*allow legacy configurations*/))
                    .setEncoding(audioFormat)
                    .setSampleRate(sampleRateInHz)
                    .build(),
                bufferSizeInBytes,
                AudioManager.AUDIO_SESSION_ID_GENERATE);
    }

这个注释写的还是比较清楚的。如果参数无效可能会抛出异常,所以创建后要通过getState()方法来判断是否可用,我们看到参数

  • audioSource 音频录制源
  • sampleRateInHz 默认采样率,单位Hz。44100Hz是当前唯一能保证在所有设备上工作的采样率,在一些设备上还有22050, 16000或11025。
  • channelConfig 描述音频通道设置
  • audioFormat 音频数据保证支持此格式。请见ENCODING_PCM_16BIT和ENCODING_PCM_8BIT。
  • bufferSizeInBytes 这个是最难理解又最重要的一个参数,它配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,一帧音频帧的大小计算如下: int size = 采样率 x 位宽 x 采样时间 x 通道数 采样时间一般取 2.5ms~120ms 之间,由厂商或者具体的应用决定,我们其实可以推断,每一帧的采样时间取得越短,产生的延时就应该会越小,当然,碎片化的数据也就会越多。在Android开发中,AudioRecord 类提供了一个帮助你确定这个 bufferSizeInBytes 的函数 设置的值比getMinBufferSize()还小则会导致初始化失败。

前面说到创建完后要通过getState()判断是否可用,判断返回值是否等于AudioRecord.STATE_INITIALIZED

第二步 开始采集

这一步很简单,直接调用startRecording()即可。

第三步 读取数据

通过read方法读取采集到的音频数据,看下方法的定义:

    //---------------------------------------------------------
    // Audio data supply
    //--------------------
    /**
     * Reads audio data from the audio hardware for recording into a byte array.
     * The format specified in the AudioRecord constructor should be
     * {@link AudioFormat#ENCODING_PCM_8BIT} to correspond to the data in the array.
     * @param audioData the array to which the recorded audio data is written.
     * @param offsetInBytes index in audioData from which the data is written expressed in bytes.
     * @param sizeInBytes the number of requested bytes.
     * @return zero or the positive number of bytes that were read, or one of the following
     *    error codes. The number of bytes will not exceed sizeInBytes.
     * <ul>
     * <li>{@link #ERROR_INVALID_OPERATION} if the object isn't properly initialized</li>
     * <li>{@link #ERROR_BAD_VALUE} if the parameters don't resolve to valid data and indexes</li>
     * <li>{@link #ERROR_DEAD_OBJECT} if the object is not valid anymore and
     *    needs to be recreated. The dead object error code is not returned if some data was
     *    successfully transferred. In this case, the error is returned at the next read()</li>
     * <li>{@link #ERROR} in case of other error</li>
     * </ul>
     */
    public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {
        return read(audioData, offsetInBytes, sizeInBytes, READ_BLOCKING);
    }

把从硬件录制采集到的音频数据读取到byte数组中。 返回值是读入缓冲区的总byte数。如果发生错误则返回值小于0,如果对象属性没有初始化,则返回ERROR_INVALID_OPERATION,如果参数不能解析成有效的数据或索引,则返回ERROR_BAD_VALUE。读取的总byte数不会超过sizeInBytes。

最后一步 释放资源

直接调用release()方法即可,对象不能经常使用此方法,而且在调用release()后,必须设置引用为null。

实战

AudioRecord 学习后,那么使用Android设备采集编码并封装输出到文件所需要的技术知识储备我们已经都具备了。现在到了如何在代码中体现的阶段了。 看到AudioRecordActivity。我们还是分步骤看:

初始化

初始化涉及两个方面,AudioRecord的创建和MediaCodec的创建

        initAudioDevice();
        try {
            mAudioEncoder = initAudioEncoder();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("audio encoder init fail");
        }

先看到initAudioDevice(),AudioRecord的创建

    private void initAudioDevice() {
        int[] sampleRates = {44100, 22050, 16000, 11025};
        for (int sampleRate : sampleRates) {
            //编码制式
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            // stereo 立体声,
            int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO;
            int buffsize = 2 * AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
            mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig,
                    audioFormat, buffsize);
            if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
                continue;
            }
            mAudioSampleRate = sampleRate;
            mAudioChanelCount = channelConfig == AudioFormat.CHANNEL_CONFIGURATION_STEREO ? 2 : 1;
            mAudioBuffer = new byte[Math.min(4096, buffsize)];
            mSampleRateType = ADTSUtils.getSampleRateType(sampleRate);
            LogUtils.w("编码器参数:" + mAudioSampleRate + " " + mSampleRateType + " " + mAudioChanelCount);
        }
    }

这里的逻辑和我们刚刚介绍的AudioRecord一致。只是循环查找有效的采样率。 这里我们设置缓冲区大小是

int buffsize =2* AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

注意这里有个关键的判断

if (mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED&&buffsize<=MAX_BUFFER_SIZE) 

为什么缓冲区大小<MAX_BUFFER_SIZE。这个大小我们在初始化编码器的时候有设置。防止采集到的数据比编码器输入缓冲区的大小还大,那就会crash掉 其他逻辑就很常规了。

接下来看到编码器初始化

    /**
     * 初始化编码器
     * @return
     * @throws IOException
     */
    private MediaCodec initAudioEncoder() throws IOException {
        MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
                mAudioSampleRate, mAudioChanelCount);
        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_BUFFER_SIZE);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 1000 * 30);
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        return encoder;
    }

注意这一句

        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_BUFFER_SIZE);

这就是前面讲的防止采集到的数据比这里还大。这里设置编码器最大的缓冲区大小。

开始采集和编码

    public void btnStart(View view) {
        initAudioDevice();
        try {
            mAudioEncoder = initAudioEncoder();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("audio encoder init fail");
        }
        //开启录音
        mRecordThread = new Thread(fetchAudioRunnable());
        try {
            mAudioBos = new BufferedOutputStream(new FileOutputStream(new File(FileUtil.getMainDir(), "record.aac")), 200 * 1024);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        presentationTimeUs = new Date().getTime() * 1000;
        mAudioRecord.startRecording();
        queue = new ArrayBlockingQueue<byte[]>(10);
        isRecord = true;
        if (mAudioEncoder != null) {
            mAudioEncoder.start();
            encodeInputBuffers = mAudioEncoder.getInputBuffers();
            encodeOutputBuffers = mAudioEncoder.getOutputBuffers();
            mAudioEncodeBufferInfo = new MediaCodec.BufferInfo();
            mEncodeThread = new Thread(new EncodeRunnable());
            mEncodeThread.start();
        }
        mRecordThread.start();
    }

这里的逻辑就是在初始化完成后,开启线程录音,然后启动编码器,开启线程进行编码 我们看到采集线程中的逻辑

    /**
     * 采集音频数据
     */
    private void fetchPcmFromDevice() {
        LogUtils.w("录音线程开始");
        while (isRecord && mAudioRecord != null && !Thread.interrupted()) {
            int size = mAudioRecord.read(mAudioBuffer, 0, mAudioBuffer.length);
            if (size < 0) {
                LogUtils.w("audio ignore ,no data to read");
                break;
            }
            if (isRecord) {
                byte[] audio = new byte[size];
                System.arraycopy(mAudioBuffer, 0, audio, 0, size);
                LogUtils.v("采集到数据:" + audio.length);
                putPCMData(audio);
            }
        }
    }

就是循环read。然后将数据添加到pcm队列中。后面的逻辑就和前面一篇文章逻辑一样了。 接下来看到编码逻辑

  /**
     * 编码PCM数据 得到MediaFormat.MIMETYPE_AUDIO_AAC格式的音频文件,并保存到
     */
    private void encodePCM() {
        int inputIndex;
        ByteBuffer inputBuffer;
        int outputIndex;
        ByteBuffer outputBuffer;
        byte[] chunkAudio;
        int outBitSize;
        int outPacketSize;
        byte[] chunkPCM;

        chunkPCM = getPCMData();//获取解码器所在线程输出的数据 代码后边会贴上
        if (chunkPCM == null) {
            return;
        }
        inputIndex = mAudioEncoder.dequeueInputBuffer(-1);//同解码器
        if (inputIndex >= 0) {
            inputBuffer = encodeInputBuffers[inputIndex];//同解码器
            inputBuffer.clear();//同解码器
            inputBuffer.limit(chunkPCM.length);
            inputBuffer.put(chunkPCM);//PCM数据填充给inputBuffer
            long pts = new Date().getTime() * 1000 - presentationTimeUs;
            LogUtils.d("开始编码: ");
            mAudioEncoder.queueInputBuffer(inputIndex, 0, chunkPCM.length, pts, 0);//通知编码器 编码
        }

        outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);//同解码器
        while (outputIndex >= 0) {//同解码器
            outBitSize = mAudioEncodeBufferInfo.size;
            outPacketSize = outBitSize + 7;//7为ADTS头部的大小
            outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
            outputBuffer.position(mAudioEncodeBufferInfo.offset);
            outputBuffer.limit(mAudioEncodeBufferInfo.offset + outBitSize);
            chunkAudio = new byte[outPacketSize];
            ADTSUtils.addADTStoPacket(mSampleRateType, chunkAudio, outPacketSize);//添加ADTS 代码后面会贴上
            outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7 你懂得
            outputBuffer.position(mAudioEncodeBufferInfo.offset);
            try {
                LogUtils.d("接受编码后数据 " + chunkAudio.length);
                mAudioBos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.aac
            } catch (IOException e) {
                e.printStackTrace();
            }
            mAudioEncoder.releaseOutputBuffer(outputIndex, false);
            outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);
        }
    }

代码差不多和上一篇文章一样,就不做过多的解释了。最终输出到文件。


到这里整个流程结束。最终得到的record.aac可以使用vlc播放器播放。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据结构与算法

洛谷P1345 [USACO5.4]奶牛的电信Telecowmunication

题目描述 农夫约翰的奶牛们喜欢通过电邮保持联系,于是她们建立了一个奶牛电脑网络,以便互相交流。这些机器用如下的方式发送电邮:如果存在一个由c台电脑组成的序列a1...

346100
来自专栏搜云库

Spring Boot 中使用 Java API 调用 Elasticsearch

ElasticSearch 是一个高可用开源全文检索和分析组件。提供存储服务,搜索服务,大数据准实时分析等。一般用于提供一些提供复杂搜索的应用。 Elastic...

2K110
来自专栏向治洪

Kotlin和anko融合进行Android开发

kotlin是一门基于jvm的编程语言,最近进行了关于kotlin和 anko的研究。并且结合现在的APP设计模式,设想了初步的开发方式。并且准备应用在新的项目...

30960
来自专栏张善友的专栏

Reactive Extensions(Rx) 学习

Bruce Eckel(著有多部编程书籍)和Jonas Boner(Akka的缔造者和Typesafe的CTO)发表了“反应性宣言”,在其中尝试着定义什么是反应...

24050
来自专栏章鱼的慢慢技术路

程序员算法时间空间复杂度速查表

16150
来自专栏菩提树下的杨过

silverlight:telerik RadControls中RadGridView的一个Bug及解决办法

当RadGridView中嵌套RadComboBox,且RadGridView的高度不够出现滚动条时,上下拉动滚动条后,RadComboBox中的选中值将丢失!...

22270
来自专栏walterlv - 吕毅的博客

使用不安全代码将 Bitmap 位图转为 WPF 的 ImageSource 以获得高性能和持续小的内存占用

发布于 2017-11-09 15:25 更新于 2017-11...

9420
来自专栏一个会写诗的程序员的博客

《Kotin 极简教程》第14章 使用 Kotlin DSL第14章 使用 Kotlin DSL《Kotlin极简教程》正式上架:

我们在前面的章节中,已经看到了 Kotlin DSL 的强大功能。例如Gradle 的配置文件 build.gradle (Groovy),以及前面我们涉及到的...

15710
来自专栏听Allen瞎扯淡

玩花招的PowerMock

当我们面对一个遗留系统时,常见的问题是没有测试。正如Michael Feathers在Working Effectively with Legacy Code一...

12320
来自专栏章鱼的慢慢技术路

程序员算法时间空间复杂度速查表

21230

扫码关注云+社区

领取腾讯云代金券