使用AudioRecord录制音频,MediaCodec编码为AAC

在实现录制音频需求的过程中的一些笔记,参考了很多有用的文章,希望能帮到他人

Android 系统 Java 层提供两个 Recorder Api, MediaRecorder 与 AudioRecorder,前者能够生成编码后的录音文件,而后者则是 PCM Audio RAW Data,我们希望通过对 PCM 的操作,根据需求完成任何操作。

AudioRecorder 基本使用方法

  1. 在 Recorder Thread 中创建 Recorder 与相关的 Encoder
  2. loop 读取 Recorder 里面的 PCM data,不断地将 PCM 喂入 Encoder中
  3. 外部停止录音后,将 run 这个 flag 置为 false 跳出循环,并且 close 相关 Encoder 并且保存他们的结果。
  4. release 相关资源。
遇到的问题:
  • 在 loop 中 Encoder的process block 时间太长,会导致来不及读取 AudioRecorder buffer,最终导致录制的音频丢失数据

  • 为了支持更多的音频格式,Recorder 就需要挂载不同的 Encoder
    所有encode的流程是相仿的,如初始化阶段,编码阶段,结束阶段,释放资源阶段。只是其中某些的具体实现步骤不同,因此通过AudioProcessor接口按照流程抽离出具体实现类encoder,将其注入到Recorder中,我们只需要挂载新的 Encoder,即可编码成我们所需要的文件格式

解决方法:
  • 所以我们创建一个ProcessThread, 让 Encoder 在 ProcessThread 中执行,这样 RecoderThread 不会因为 Encoder 而 block 导致数据丢失。

  • 由于项目的需要,要把pcm转码为aac音频,目前大致两种方案,FFmpeg和MediaCodec,我们这里使用MediaCodec

接下来就是具体的encode阶段,先初始化编码器 ,和解码器的MediaFormat直接在音频文件内获取不同,编码器的MediaFormat需要自己来创建,对MediaCodec还不是很了解的同学,可以参考这篇文章http://www.jianshu.com/p/30e596112015
下文引用了很多部分

用编码器把PCM转为AAC

我们在AudioProcessor的初始化方法中,完成创建输出文件和初始化编码器的步骤.创建了一个MediaCodec对象,此时MediaCodec处于Uninitialized状态。首先,需要使用configure(…)方法对MediaCodec进行配置,这时MediaCodec转为Configured状态。然后调用start()方法使其转入Executing状态。

@Override
public void start() {

        try {
            fos = new FileOutputStream(filePath);
            bos = new BufferedOutputStream(fos, 200 * 1024);

        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 16000, 1);//参数对应-> mime type、采样率、声道数
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128 * 100);//比特率
            encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16*1024);
            codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            codec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (codec == null) {
            Log.e(TAG, "create mediaEncode failed");
            return;
        }
        
        //调用MediaCodec的start()方法,此时MediaCodec处于Executing状态
        codec.start();

}


初始化结束我们就可以loop喂数据给我们的AudioProcessor,AudioProcessor会调用我们的这个方法,进行具体的编码工作.

Executing状态包含三个子状态: Flushed、 Running 以及End-of-Stream。

  1. 在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。
  2. 一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。
  3. 当你将一个带有end-of-stream marker标记的输入缓存入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入缓存,但它仍然产生输出缓存直到end-of- stream标记输出。

 @Override
public void flow(byte[] bytes, int size) {
        int inputIndex;
        ByteBuffer inputBuffer;
        int outputIndex;
        ByteBuffer outputBuffer;
        byte[] chunkAudio;
        int outBitSize;
        int outPacketSize;
        //通过getInputBuffers()方法和getOutputBuffers()方法获取缓存队列
        encodeInputBuffers = codec.getInputBuffers();
        encodeOutputBuffers = codec.getOutputBuffers();
        //用于存储ByteBuffer的信息
        encodeBufferInfo = new MediaCodec.BufferInfo();
        
        //首先通过dequeueInputBuffer(long timeoutUs)请求一个输入缓存,timeoutUs代表等待时间,设置为-1代表无限等待
        int inputBufferIndex = codec.dequeueInputBuffer(-1);
        
        //返回的整型变量为请求到的输入缓存的index,通过getInputBuffers()得到的输入缓存数组,再用index和输入缓存数组即可得到当前请求的输入缓存
        if (inputBufferIndex >= 0) {
            inputBuffer = encodeInputBuffers[inputBufferIndex];
            //使用之前要clear一下,避免之前的缓存数据影响当前数据
            inputBuffer.clear();
            //把数据添加到输入缓存中,
            inputBuffer.put(bytes);
            //并调用queueInputBuffer()把缓存数据入队
            codec.queueInputBuffer(inputBufferIndex, 0, size, 0, 0);
        }
        //通过dequeueOutputBuffer(BufferInfo info, long timeoutUs)来请求一个输出缓存,传入一个上面的BufferInfo对象
        outputIndex = codec.dequeueOutputBuffer(encodeBufferInfo, 10000);
        //然后通过返回的index得到输出缓存,并通过BufferInfo获取ByteBuffer的信息
        while (outputIndex >= 0) {
            outBitSize = encodeBufferInfo.size;
            
            //添加ADTS头,ADTS头包含了AAC文件的采样率、通道数、帧数据长度等信息。
            outPacketSize = outBitSize + 7;//7为ADTS头部的大小
            outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
            outputBuffer.position(encodeBufferInfo.offset);
            outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
            chunkAudio = new byte[outPacketSize];
            addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代码后面会贴上
            outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中偏移量offset=7 
            outputBuffer.position(encodeBufferInfo.offset);
            //showLog("outPacketSize:" + outPacketSize + " encodeOutBufferRemain:" + outputBuffer.remaining());
            try {
                bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.aac
            } catch (IOException e) {
                e.printStackTrace();
            }
            //releaseOutputBuffer方法必须调用
            codec.releaseOutputBuffer(outputIndex, false);
            outputIndex = codec.dequeueOutputBuffer(encodeBufferInfo, 10000);

        }
    }
    
    
     /**
     * 添加ADTS头
     *
     * @param packet
     * @param packetLen
     */
    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2; // AAC LC
        int freqIdx = 8; // 44.1KHz
        int chanCfg = 1; // CPE


        // fill in ADTS data
        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }

结束MediaCodec,并释放掉占用资源

 @Override
public void end() {
        try {
            if (bos != null) {
                bos.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    bos=null;
                }
            }
        }

        try {
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            fos=null;
        }

        if (codec != null) {
            codec.stop();
            codec.release();
            codec=null;
        }

}

参考:
https://juejin.im/entry/58fd31b75c497d005802c5e3
http://www.jianshu.com/p/30e596112015

    原文作者:口袋解忧铺
    原文地址: https://www.jianshu.com/p/12d491bf286f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞