使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容