Android AVDemo(2):音频编码,采集 PCM 数据编码为 AAC丨音视频工程示例

vx 搜索『gjzkeyframe』 关注『关键帧Keyframe』来及时获得最新的音视频技术文章。

塞尚《静物》

这个公众号会路线图 式的遍历分享音视频技术音视频基础(完成)音视频工具(完成)音视频工程示例(进行中) → 音视频工业实战(准备)。

iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染过程,并借助音视频工具来分析和理解对应的音视频数据。

音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。

这里是 Android 第二篇:Android 音频编码 Demo。这个 Demo 里包含以下内容:

  • 1)实现一个音频采集模块;
  • 2)实现一个音频编码模块;
  • 3)串联音频采集和编码模块,将采集到的音频数据输入给 AAC 编码模块进行编码和存储;
  • 4)详尽的代码注释,帮你理解代码逻辑和原理;

1、音频采集模块

在这个 Demo 中,音频采集模块 KFAudioCapture 的实现与 Android 音频采集 Demo 中一样,这里就不再重复介绍了,其接口如下:

KFAudioCapture.java

public class KFAudioCapture {
 public KFAudioCapture(KFAudioCaptureConfig config,KFAudioCaptureListener listener);
 public void startRunning(); ///< 开始采集音频数据。
 public void stopRunning(); ///< 停止采集音频数据。
 public void release(); ///< 释放音频采集。
}

2、音频编码模块

我们定义了接口类 KFMediaCodecInterface,后续编解码模块实现这个接口即可。需要关注 setup 接口的参数 isEncoder 代表是否使用编码功能,mediaFormat 代表输入数据格式描述。

KFMediaCodecInterface.java

public interface KFMediaCodecInterface {
    public static final int KFMediaCodecInterfaceErrorCreate = -2000;
    public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
    public static final int KFMediaCodecInterfaceErrorStart = -2002;
    public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
    public static final int KFMediaCodecInterfaceErrorParams = -2004;

    public static int KFMediaCodeProcessParams = -1;
    public static int KFMediaCodeProcessAgainLater = -2;
    public static int KFMediaCodeProcessSuccess = 0;

    ///< 初始化 Codec,第一个参数需告知使用编码还是解码。
    public void setup(boolean isEncoder, MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
    ///< 释放 Codec。
    public void release();

    ///< 获取输出格式描述。
    public MediaFormat getOutputMediaFormat();
    ///< 获取输入格式描述。
    public MediaFormat getInputMediaFormat();
    ///< 处理每一帧数据,编码前与编码后都可以,支持编解码 2 种模式。
    public int processFrame(KFFrame frame);
    ///< 清空 Codec 缓冲区。
    public void flush();
}

接下来,我们来实现一个音频编码模块 KFByteBufferCodec,需要实现上面的接口 KFMediaCodecInterface,在这里输入采集后的数据,输出编码后的数据。这里命名为 KFByteBufferCodec,主要因为它可以支持音视频编解码多个功能。

KFByteBufferCodec.java

public class KFByteBufferCodec implements KFMediaCodecInterface {
    public static final int KFByteBufferCodecErrorParams = -2500;
    public static final int KFByteBufferCodecErrorCreate = -2501;
    public static final int KFByteBufferCodecErrorConfigure = -2502;
    public static final int KFByteBufferCodecErrorStart = -2503;

    private static final int KFByteBufferCodecInputBufferMaxCache = 20 * 1024 * 1024;
    private static final String TAG = "KFByteBufferCodec";
    private KFMediaCodecListener mListener = null; ///< 回调
    private MediaCodec mMediaCodec = null; ///< Codec 实例
    private ByteBuffer[] mInputBuffers; ///<  Codec 输入缓冲区
    private MediaFormat mInputMediaFormat = null; ///< 输入数据格式描述
    private MediaFormat mOutMediaFormat = null; ///< 输出数据格式描述

    private long mLastInputPts = 0; ///< 上一帧时间戳
    private List<KFBufferFrame> mList = new ArrayList<>(); ///< 输入数据缓存
    private int mListCacheSize = 0; ///< 输入数据缓存数量
    private ReentrantLock mListLock = new ReentrantLock(true); ///< 数据缓存锁
    private boolean mIsEncoder = true;

    private HandlerThread mCodecThread = null; ///< Codec 线程
    private Handler mCodecHandler = null;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主线程

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext) {
        mListener = listener;
        mInputMediaFormat = mediaFormat;
        mIsEncoder = isEncoder;

        mCodecThread = new HandlerThread("KFByteBufferCodecThread");
        mCodecThread.start();
        mCodecHandler = new Handler((mCodecThread.getLooper()));

        mCodecHandler.post(()->{
            if(mInputMediaFormat == null){
                _callBackError(KFByteBufferCodecErrorParams,"mInputMediaFormat null");
                return;
            }
            ///< 初始化 Codec 实例。
            _setupCodec();
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public void  release() {
        ///< 释放 Codec 实例、输入缓存。
        mCodecHandler.post(()-> {
            if(mMediaCodec != null){
                try {
                    mMediaCodec.stop();
                    mMediaCodec.release();
                } catch (Exception e) {
                    Log.e(TAG, "release: " + e.toString());
                }
                mMediaCodec = null;
            }

            mListLock.lock();
            mList.clear();
            mListCacheSize = 0;
            mListLock.unlock();

            mCodecThread.quit();
        });
    }

    @Override
    public MediaFormat getOutputMediaFormat() {
        return mOutMediaFormat;
    }

    @Override
    public MediaFormat getInputMediaFormat() {
        return mInputMediaFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public int processFrame(KFFrame inputFrame) {
        ///< 处理输入帧数据。
        if(inputFrame == null){
            return KFMediaCodeProcessParams;
        }

        KFBufferFrame frame = (KFBufferFrame)inputFrame;
        if(frame.buffer ==null || frame.bufferInfo == null || frame.bufferInfo.size == 0){
            return KFMediaCodeProcessParams;
        }

        ///< 先添加到缓冲区,一旦缓冲区满则返回 KFMediaCodeProcessAgainLater。
        boolean appendSuccess = _appendFrame(frame);
        if(!appendSuccess){
            return KFMediaCodeProcessAgainLater;
        }

        mCodecHandler.post(()-> {
            if(mMediaCodec == null){
                return;
            }

            ///< 子线程处理编解码,从队列取出一组数据,能塞多少就塞多少数据。
            mListLock.lock();
            int mListSize = mList.size();
            mListLock.unlock();
            while (mListSize > 0){
                mListLock.lock();
                KFBufferFrame packet = mList.get(0);
                mListLock.unlock();

                int bufferIndex;
                try {
                    bufferIndex = mMediaCodec.dequeueInputBuffer(10 * 1000);
                } catch (Exception e) {
                    Log.e(TAG, "dequeueInputBuffer" + e);
                    return;
                }

                if (bufferIndex >= 0) {
                    mInputBuffers[bufferIndex].clear();
                    mInputBuffers[bufferIndex].put(packet.buffer);
                    mInputBuffers[bufferIndex].flip();
                    try {
                        mMediaCodec.queueInputBuffer(bufferIndex, 0, packet.bufferInfo.size, packet.bufferInfo.presentationTimeUs, packet.bufferInfo.flags);
                    } catch (Exception e) {
                        Log.e(TAG, "queueInputBuffer" + e);
                        return;
                    }

                    mLastInputPts = packet.bufferInfo.presentationTimeUs;
                    mListLock.lock();
                    mList.remove(0);
                    mListSize = mList.size();
                    mListCacheSize -= packet.bufferInfo.size;
                    mListLock.unlock();
                } else {
                    break;
                }
            }

            ///< 获取 Codec 后的数据,一样的策略,尽量拿出最多的数据出来,回调给外层。
            long outputDts = -1;
            MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo();
            while (outputDts < mLastInputPts) {
                int bufferIndex;
                try {
                    bufferIndex = mMediaCodec.dequeueOutputBuffer(outputBufferInfo, 10 * 1000);
                } catch (Exception e) {
                    Log.e(TAG, "dequeueOutputBuffer" + e);
                    return;
                }

                if (bufferIndex >= 0) {
                    ByteBuffer decodeBuffer = mMediaCodec.getOutputBuffer(bufferIndex);
                    if (mListener != null) {
                        KFBufferFrame bufferFrame = new KFBufferFrame(decodeBuffer,outputBufferInfo);
                        mListener.dataOnAvailable(bufferFrame);
                    }
                    mMediaCodec.releaseOutputBuffer(bufferIndex,true);
                } else {
                    if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        mOutMediaFormat = mMediaCodec.getOutputFormat();
                    }
                    break;
                }
            }
        });

        return KFMediaCodeProcessSuccess;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public void flush() {
        ///< Codec 清空缓冲区,一般用于Seek、结束时时使用。
        mCodecHandler.post(()-> {
            if (mMediaCodec == null) {
                return;
            }

            try {
                mMediaCodec.flush();
            } catch (Exception e) {
                Log.e(TAG, "flush" + e);
            }

            mListLock.lock();
            mList.clear();
            mListCacheSize = 0;
            mListLock.unlock();
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private boolean _appendFrame(KFBufferFrame frame) {
        ///< 将输入数据添加至缓冲区。
        mListLock.lock();
        int cacheSize = mListCacheSize;
        mListLock.unlock();
        if(cacheSize >= KFByteBufferCodecInputBufferMaxCache){
            return false;
        }

        KFBufferFrame packet = new KFBufferFrame();

        ByteBuffer newBuffer = ByteBuffer.allocateDirect(frame.bufferInfo.size);
        newBuffer.put(frame.buffer).position(0);
        MediaCodec.BufferInfo newInfo = new MediaCodec.BufferInfo();
        newInfo.size = frame.bufferInfo.size;
        newInfo.flags = frame.bufferInfo.flags;
        newInfo.presentationTimeUs = frame.bufferInfo.presentationTimeUs;
        packet.buffer = newBuffer;
        packet.bufferInfo = newInfo;

        mListLock.lock();
        mList.add(packet);
        mListCacheSize += packet.bufferInfo.size;
        mListLock.unlock();

        return true;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private boolean _setupCodec() {
        ///< 初始化 Codec 模块,支持编码、解码,根据不同 MediaFormat 创建不同 Codec。
        try {
            String mimetype = mInputMediaFormat.getString(MediaFormat.KEY_MIME);
            if (mIsEncoder) {
                mMediaCodec = MediaCodec.createEncoderByType(mimetype);
            } else {
                mMediaCodec = MediaCodec.createDecoderByType(mimetype);
            }
        } catch (Exception e) {
            Log.e(TAG, "createCodecByType" + e + mIsEncoder);
            _callBackError(KFByteBufferCodecErrorCreate,e.getMessage());
            return false;
        }

        try {
            mMediaCodec.configure(mInputMediaFormat, null, null, mIsEncoder ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0);
        } catch (Exception e) {
            Log.e(TAG, "configure" + e);
            _callBackError(KFByteBufferCodecErrorConfigure,e.getMessage());
            return false;
        }

        try {
            mMediaCodec.start();
            mInputBuffers = mMediaCodec.getInputBuffers();
        } catch (Exception e) {
            Log.e(TAG, "start" +  e );
            _callBackError(KFByteBufferCodecErrorStart,e.getMessage());
            return false;
        }

        return true;
    }

    private void _callBackError(int error, String errorMsg) {
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.onError(error,TAG + errorMsg);
            });
        }
    }
}

上面是 KFByteBufferCodec 的实现,从代码上可以看到主要有这几个部分:

  • 1)创建与开启编码实例,_setupCodec,调用 setup: 时才会创建编码实例。
    • mIsEncoder 为 true 代表使用编码功能,创建编码功能使用 createEncoderByType,创建解码使用 createDecoderByTypeconfigure 配置 Codec 编码使用 MediaCodec.CONFIGURE_FLAG_ENCODE,解码则填 0 即可。
    • start_setupCodec 中执行,开启音频编码。
  • 2)停止与清理编码实例,release
    • stoprelease 中执行,关闭音频编码。
  • 3)刷新编码缓冲区,flush,通常编码结束时将缓冲区数据刷新出来。
  • 4)处理音频编码数据,processFrame,将编码前数据放入缓冲区,编码后数据抛给外层。
    • 输入缓冲区队列为 mList,需要注意缓冲区有上限,一旦超过最大值则返回 KFMediaCodeProcessAgainLater,防止因内存问题导致 OOM。
    • 编码线程异步处理数据,从 mList 取出数据塞入尽量多的数据给编码器,这样跳出循环条件为塞入编码器失败或者 mList 为空。拉取数据是借助标记 mList 最后一帧时间戳 mLastInputPts,跳出循环条件为输出数据等于此时间戳或拉取数据失败。MediaCodec 采用异步方式处理数据,并且使用了一组输入输出缓存 mInputBuffers。通过请求一个空的输入缓存 dequeueInputBuffer,向其中填充满数据并将它传递给编解码器处理 queueInputBuffer。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存中 dequeueOutputBuffer。使用完输出缓存的数据之后 getOutputBuffer,将其释放回编解码器 releaseOutputBuffer。具体流程如下图所示:
MediaCodec
我们又定义了类 `KFAudioByteBufferEncoder`,继承自 `KFByteBufferCodec`,重写了 `processFrame` `release` `flush` 三个方法。

KFAudioByteBufferEncoder.java

public class KFAudioByteBufferEncoder extends KFByteBufferCodec {
    private int mChannel = 0; ///< 音频声道数
    private int mSampleRate = 0; ///< 音频采样率
    private long mCurrentTimestamp = -1; ///< 标记当前时间戳 (因为数据重新分割,所以时间戳需要手动计算)
    private byte[] mByteArray = new byte[500 * 1024]; ///< 输入音频数据数组
    private int mByteArraySize = 0; ///< 输入音频数据 Size

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public int processFrame(KFFrame inputFrame) {
        ///< 获取音频声道数与采样率。
        if (mChannel == 0) {
            MediaFormat inputMediaFormat = getInputMediaFormat();
            if (inputMediaFormat != null) {
                mChannel = inputMediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                mSampleRate = inputMediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
            }
        }

        if (mChannel == 0  || mSampleRate == 0 || inputFrame == null) {
            return KFMediaCodeProcessParams;
        }

        KFBufferFrame bufferFrame = (KFBufferFrame)inputFrame;
        if (bufferFrame.bufferInfo == null || bufferFrame.bufferInfo.size == 0) {
            return KFMediaCodeProcessParams;
        }

        ///< 控制音频输入给编码器单次字节数 2048 字节。
        int sendSize = 2048;
        ///< 外层输入如果为 2048 则直接跳过执行。
        if (mByteArraySize == 0 && sendSize == bufferFrame.bufferInfo.size) {
            return super.processFrame(inputFrame);
        } else {
            long currentTimestamp = 0;
            if (mCurrentTimestamp == -1) {
                mCurrentTimestamp = bufferFrame.bufferInfo.presentationTimeUs;
            }

            ///< 将缓存中数据执行送入编码器操作。
            int sendCacheStatus = sendBufferEncoder(sendSize);
            if (sendCacheStatus < 0) {
                return sendCacheStatus;
            }

            ///< 将输入数据送入缓冲区重复执行此操作。
            byte[] inputBytes = new byte[bufferFrame.bufferInfo.size];
            bufferFrame.buffer.get(inputBytes);

            System.arraycopy(inputBytes,0,mByteArray,mByteArraySize,bufferFrame.bufferInfo.size);
            mByteArraySize += bufferFrame.bufferInfo.size;

            return sendBufferEncoder(sendSize);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void release() {
        mCurrentTimestamp = -1;
        mByteArraySize = 0;
        super.release();
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void flush() {
        mCurrentTimestamp = -1;
        mByteArraySize = 0;
        super.flush();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private int sendBufferEncoder(int sendSize) {
        ///< 将当前 Buffer 中数据按每次 2048 送给编码器。
        while (mByteArraySize >= sendSize) {
            MediaCodec.BufferInfo newBufferInfo = new MediaCodec.BufferInfo();
            newBufferInfo.size = sendSize;
            newBufferInfo.presentationTimeUs = mCurrentTimestamp;

            ByteBuffer newBuffer = ByteBuffer.allocateDirect(sendSize);
            newBuffer.put(mByteArray,0,sendSize).position(0);

            KFBufferFrame newFrame = new KFBufferFrame();
            newFrame.buffer = newBuffer;
            newFrame.bufferInfo = newBufferInfo;
            int status = super.processFrame(newFrame);
            if (status < 0) {
                return status;
            } else {
                mByteArraySize -= sendSize;
                if (mByteArraySize > 0) {
                    System.arraycopy(mByteArray, sendSize, mByteArray, 0, mByteArraySize);
                }
            }
            mCurrentTimestamp += sendSize * 1000000 / (16 / 8 * mSampleRate * mChannel);
        }
        return KFMediaCodeProcessSuccess;
    }
}

上面是 KFAudioByteBufferEncoder 的实现,主要就干了一件事:拆分合适大小(2048 字节)的数据送给编码器。因为 AAC 数据编码每 packet 大小为 1024 * 2(位深 16 Bit)

3、采集音频数据进行 AAC 编码和存储

我们在一个 MainActivity 中来实现音频采集及编码逻辑,并将编码后的数据加上 ADTS[1] 头信息存储为 AAC 数据。

关于 ADTS,在《音频编码:PCM 和 AAC 编码》中也有介绍,可以去看看了解一下。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private FileOutputStream mStream = null;
    private KFAudioCapture mAudioCapture = null; ///< 音频采集模块
    private KFAudioCaptureConfig mAudioCaptureConfig = null; ///< 音频采集配置
    private KFMediaCodecInterface mEncoder = null; ///< 音频编码
    private MediaFormat mAudioEncoderFormat = null; ///< 音频编码格式描述
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        mAudioCaptureConfig = new KFAudioCaptureConfig();
        mAudioCapture = new KFAudioCapture(mAudioCaptureConfig,mAudioCaptureListener);
        mAudioCapture.startRunning();

        if (mStream == null) {
            try {
                mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.aac");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }

        FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
        startParams.gravity = Gravity.CENTER_HORIZONTAL;
        Button startButton = new Button(this);
        startButton.setTextColor(Color.BLUE);
        startButton.setText("开始");
        startButton.setVisibility(View.VISIBLE);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mEncoder == null) {
                    mEncoder = new KFAudioByteBufferEncoder();
                    MediaFormat mediaFormat = KFAVTools.createAudioFormat(mAudioCaptureConfig.sampleRate,mAudioCaptureConfig.channel,96*1000);
                    mEncoder.setup(true,mediaFormat,mAudioEncoderListener,null);
                    ((Button)view).setText("停止");
                } else {
                    mEncoder.release();
                    mEncoder = null;
                    ((Button)view).setText("开始");
                }
            }
        });
        addContentView(startButton, startParams);
    }

    private KFAudioCaptureListener mAudioCaptureListener = new KFAudioCaptureListener() {
        @Override
        public void onError(int error, String errorMsg) {
            Log.e("KFAudioCapture","errorCode" + error + "msg"+errorMsg);
        }

        @Override
        public void onFrameAvailable(KFFrame frame) {
            if (mEncoder != null) {
                mEncoder.processFrame(frame);
            }
        }
    };

    private KFMediaCodecListener mAudioEncoderListener = new KFMediaCodecListener() {
        @Override
        public void onError(int error, String errorMsg) {
            Log.i("KFMediaCodecListener","error" + error + "msg" + errorMsg);
        }

        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public void dataOnAvailable(KFFrame frame) {
            ///< 音频回调数据
            if (mAudioEncoderFormat == null && mEncoder != null) {
                mAudioEncoderFormat = mEncoder.getOutputMediaFormat();
            }
            KFBufferFrame bufferFrame = (KFBufferFrame)frame;
            try {
                ///< 添加ADTS数据
                ByteBuffer adtsBuffer = KFAVTools.getADTS(bufferFrame.bufferInfo.size,mAudioEncoderFormat.getInteger(MediaFormat.KEY_PROFILE),mAudioEncoderFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),mAudioEncoderFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
                byte[] adtsBytes = new byte[adtsBuffer.capacity()];
                adtsBuffer.get(adtsBytes);
                mStream.write(adtsBytes);

                byte[] dst = new byte[bufferFrame.bufferInfo.size];
                bufferFrame.buffer.get(dst);
                mStream.write(dst);
            }  catch (IOException e) {
                e.printStackTrace();
            }
        }
    };
}

上面是 MainActivity的实现,其中主要包含这几个部分:

  • 1)在采集音频前需要设置 Manifest.permission.RECORD_AUDIO 权限。
  • 2)通过启动和停止音频采集来驱动整个采集和编码流程。
  • 3)在采集模块 KFAudioCapture 的数据回调中将数据交给编码模块 KFAudioByteBufferEncoder 进行编码。
    • KFAudioCaptureListeneronFrameAvailable 回调中实现。
  • 4)创建模块 KFAudioByteBufferEncodersetup 中 MediaFormat。
    • 对应的实现在 KFAVTools 类的工具方法 static MediaFormat createVideoFormat(boolean isHEVC, Size size,int format,int bitrate,int fps,int gopDuration,int profile,int profileLevel) 中实现。
  • 5)在编码模块 KFAudioByteBufferEncoder的数据回调中获取编码后的 AAC 裸流数据,并在每个 AAC packet 前写入 ADTS 头数据,存储到文件中。
    • KFMediaCodecListenerdataOnAvailable 回调中实现。
    • 其中生成一个 AAC packet 对应的 ADTS 头数据在 KFAVTools 类的工具方法 static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel) 中实现。

KFAVTools.java

public class KFAVTools {

    // 按音频参数生产 AAC packet 对应的 ADTS 头数据。
    // 当编码器编码的是 AAC 裸流数据时,需要在每个 AAC packet 前添加一个 ADTS 头用于解码器解码音频流。
    // 参考文档:
    // ADTS 格式参考:http://wiki.multimedia.cx/index.php?title=ADTS
    // MPEG-4 Audio 格式参考:http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations
    public static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel) {
        int sampleRateIndex = getSampleRateIndex(sampleRate);// 取得采样率对应的 index。
        int fullSize = 7 + size;
        // ADTS 头固定 7 字节。
        // 填充 ADTS 数据。
        ByteBuffer adtsBuffer = ByteBuffer.allocateDirect(7);
        adtsBuffer.order(ByteOrder.nativeOrder());
        adtsBuffer.put((byte)0xFF); // 11111111 = syncword
        adtsBuffer.put((byte)0xF1);
        adtsBuffer.put((byte)(((profile - 1) << 6) + (sampleRateIndex << 2) + (channel >> 2)));
        adtsBuffer.put((byte)(((channel & 3) << 6) + (fullSize >> 11)));
        adtsBuffer.put((byte)((fullSize & 0x7FF) >> 3));
        adtsBuffer.put((byte)(((fullSize & 7) << 5) + 0x1F));
        adtsBuffer.put((byte)0xFC);
        adtsBuffer.position(0);

        return adtsBuffer;
    }

    private static int getSampleRateIndex(int sampleRate) {
        int sampleRateIndex = 0;
        switch (sampleRate) {
            case 96000:
                sampleRateIndex = 0;
                break;
            case 88200:
                sampleRateIndex = 1;
                break;
            case 64000:
                sampleRateIndex = 2;
                break;
            case 48000:
                sampleRateIndex = 3;
                break;
            case 44100:
                sampleRateIndex = 4;
                break;
            case 32000:
                sampleRateIndex = 5;
                break;
            case 24000:
                sampleRateIndex = 6;
                break;
            case 22050:
                sampleRateIndex = 7;
                break;
            case 16000:
                sampleRateIndex = 8;
                break;
            case 12000:
                sampleRateIndex = 9;
                break;
            case 11025:
                sampleRateIndex = 10;
                break;
            case 8000:
                sampleRateIndex = 11;
                break;
            case 7350:
                sampleRateIndex = 12;
                break;
            default:
                sampleRateIndex = 15;
        }
        return sampleRateIndex;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public static MediaFormat createVideoFormat(boolean isHEVC, Size size,int format,int bitrate,int fps,int gopDuration,int profile,int profileLevel) {
        String mimeType = isHEVC ? "video/hevc" : "video/avc";
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(mimeType, size.getWidth(), size.getHeight());

        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, format); //MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, gopDuration);
        mediaFormat.setInteger(MediaFormat.KEY_PROFILE, profile);
        mediaFormat.setInteger(MediaFormat.KEY_LEVEL, profileLevel);

        return mediaFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public static MediaFormat createAudioFormat(int sampleRate, int channel, int bitrate) {
        String mimeType = MediaFormat.MIMETYPE_AUDIO_AAC;
        MediaFormat mediaFormat = MediaFormat.createAudioFormat(mimeType, sampleRate, channel);

        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
        mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channel);
        mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);
        mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);

        return mediaFormat;
    }
}

3、用工具播放 AAC 文件

完成音频采集和编码后,可以将 sdcard 文件夹下面的 test.aac 文件拷贝到电脑上,使用 ffplay 播放来验证一下音频采集是效果是否符合预期:

$ ffplay -I test.aac

这里在播放 AAC 文件时不必像播放 PCM 文件那样设置音频参数,这正是因为我们已经将对应的参数信息编码到 ADTS 头部数据中去了,播放解码时可以从中解析出这些信息从而正确的解码 AAC。

关于播放 AAC 文件的工具,可以参考《FFmpeg 工具》第 2 节 ffplay 命令行工具《可视化音视频分析工具》第 1.1 节 Adobe Audition

参考资料

[1] ADTS 格式: http://wiki.multimedia.cx/index.php?title=ADTS

推荐阅读

《Android AVDemo(1):音频采集》

《iOS AVDemo(2):音频编码》

《iOS AVDemo(1):音频采集》

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

推荐阅读更多精彩内容