zoukankan      html  css  js  c++  java
  • JavaCV FFmpeg AAC编码

    上次成功通过FFmpeg采集麦克风的PCM数据,这次针对上一次的程序进行了改造,使用AAC编码采集后的数据。

    (传送门) JavaCV FFmpeg采集麦克风PCM音频数据

    采集麦克风数据是一个解码过程,而将采集后的数据进行AAC编码则是编码过程,如图:

    从上图可以看出,编码过程,数据流是从AVFrame流向AVPacket,而解码过程正好相反,数据流是从AVPacket流向AVFrame。

    javacpp-ffmpeg依赖:

    <dependency>
        <groupId>org.bytedeco.javacpp-presets</groupId>
        <artifactId>ffmpeg</artifactId>
        <version>${ffmpeg.version}</version>
    </dependency>
    

    FFmpeg编码的过程是解码的逆过程,不过主线流程是类似的,如下图:

    基本上主要的步骤都是:

    1. 查找编码/解码器
    2. 打开编码/解码器
    3. 进行编码/解码

    在FFmpeg的demo流程中其实还有创建流avformat_new_stream(),写入头部信息avformat_write_header()和尾部信息av_write_trailer()等操作,这里只是将PCM数据编码成AAC,所以可以暂时不需要考虑这些操作。

    将采集音频流数据进行AAC编码的整体流程主要有以下几个步骤:

    1. 采集音频帧
    2. 将视音频帧重采样
    3. 构建AAC编码器
    4. 对音频帧进行编码
    采集音频帧

    采集音频流中的音频帧在上一次采集PCM数据的时候已经实现了,主要是从AVFormatContext中用av_read_frame()读取音频数据并进行解码(avcodec_decode_audio4()),实现代码如下:

    public AVFrame grab() throws FFmpegException {
        if (av_read_frame(pFormatCtx, pkt) >= 0 && pkt.stream_index() == audioIdx) {
            ret = avcodec_decode_audio4(pCodecCtx, pFrame, got, pkt);
            if (ret < 0) {
                throw new FFmpegException(ret, "avcodec_decode_audio4 解码失败");
            }
            if (got[0] != 0) {
                return pFrame;
            }
            av_packet_unref(pkt);
        }
        return null;
    }
    

    这样通过grab()方法就可以获取到音频流中的音频帧了。

    音频帧重采样

    在进行AAC编码之前,如果采集的音频帧信息格式跟编码器信息不一致则需要进行重采样,用到的是FFmpeg的SwrContext组件,下面的AudioConverter是对SwrContext封装的组件,内部实现了AVFrame的填充及SwrContext的初始化,使用方式如下:

    // 1. 创建AudioConverter,指定转化格式为AV_SAMPLE_FMT_S16
    AudioConverter.create(src_channel_layout, src_sample_fmt, src_sample_rate, 
        dst_channel_layout, AV_SAMPLE_FMT_S16, dst_sample_rate, dst_nb_samples);
    // 2. 对音频帧进行转化swr_convert
    converter.convert(pFrame);
    

    AudioConverter的convert方式,实际上也是调用了SwrContext的swr_convert方法:

    swr_convert(swrCtx, new PointerPointer<>(buffer), bufferLen, pFrame.data(), pFrame.nb_samples());
    
    构建AAC编码器

    进行AAC编码之前需要构建AAC编码器,根据上面的流程图利用avcodec_find_encoder()avcodec_alloc_context3()实现编码器的创建和参数配置,最后用avcodec_open()打开编码器,完整的初始化代码如下:

    public static AudioAACEncoder create(int channels, int sample_fmt, int sample_rate, Consumer<byte[]> aacBufConsumer, Map<String, String> opts) throws FFmpegException {
        AudioAACEncoder a = new AudioAACEncoder();
        // 查找AAC编码器
        a.pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
        if (a.pCodec == null) {
            throw new FFmpegException("初始化 AV_CODEC_ID_AAC 编码器失败");
        }
        // 初始化编码器信息
        a.pCodecCtx = avcodec_alloc_context3(a.pCodec);
        a.pCodecCtx.codec_id(AV_CODEC_ID_AAC);
        a.pCodecCtx.codec_type(AVMEDIA_TYPE_AUDIO);
        a.pCodecCtx.sample_fmt(sample_fmt);
        a.pCodecCtx.sample_rate(sample_rate);
        a.pCodecCtx.channel_layout(av_get_default_channel_layout(channels));
        // 音频参数设置
        a.pCodecCtx.channels(av_get_channel_layout_nb_channels(a.pCodecCtx.channel_layout()));
        a.pCodecCtx.bit_rate(64000);
        // 其他参数设置
        AVDictionary dictionary = new AVDictionary();
        opts.forEach((k, v) -> av_dict_set(dictionary, k, v, 0));
        a.ret = avcodec_open2(a.pCodecCtx, a.pCodec, dictionary);
        if (a.ret < 0) {
            throw new FFmpegException(a.ret, "avcodec_open2 编码器打开失败");
        }
        // 填充音频帧
        a.aacFrame = av_frame_alloc();
        a.aacFrame.nb_samples(a.pCodecCtx.frame_size());
        a.aacFrame.format(a.pCodecCtx.sample_fmt());
        a.aacFrameSize = av_samples_get_buffer_size((IntPointer) null, a.pCodecCtx.channels(), //
            a.pCodecCtx.frame_size(), a.pCodecCtx.sample_fmt(), 1);
        // pCodecCtx.sample_fmt() = S16
        // AutoCloseable
        a.buffer = new BytePointer(av_malloc(a.aacFrameSize)).capacity(a.aacFrameSize);
        avcodec_fill_audio_frame(a.aacFrame, a.pCodecCtx.channels(), a.pCodecCtx.sample_fmt(), a.buffer, a.aacFrameSize, 1);
    
        a.pkt = new AVPacket();
        a.pcmBuffer = new byte[DEF_PCM_BUFFER_SIZE];
        a.aacBuffConsumer = aacBufConsumer;
        return a;
    }
    

    这里需要特别注意的是,不是每一帧pcm数据都能编码成为一帧AAC音频帧,所以这里通过Consumer<byte[]> aacBufConsumer指定回调来消费编码完成的AAC音频帧。

    对音频帧进行编码

    编码器构建完成后就可以对音频帧进行编码了,入参为AVFrame,出参通过Consumer<byte[]> aacBufConsumer指定回调输出byte[],就如上面提到,不是一帧PCM音频数据就能编码成一帧AAC数据,所以这里需要就多帧pcm音频帧进行编码,并缓存未编码的pcm数据留到下一次编码。

    public void encode(AVFrame avFrame) throws FFmpegException {
        // 计算Pcm容量
        int size = AudioUtils.toPcmFrameSize(avFrame, pCodecCtx.channels(), pCodecCtx.sample_fmt());
        byte[] buff = new byte[size];
        avFrame.data(0).get(buff);
    
        System.arraycopy(buff, 0, pcmBuffer, offset, size);
        offset += size;
        capacity += size;
    
        while (capacity >= aacFrameSize) {
            byte[] aacBuf = new byte[aacFrameSize];
            System.arraycopy(pcmBuffer, 0, aacBuf, 0, aacFrameSize);
            aacFrame.data(0).put(aacBuf);
            // 减去已经用于编码的buff
            capacity -= aacFrameSize;
            offset = capacity;
            if (capacity > 0) { // 如果还有剩余,则放入buffer最前面
                byte[] lBuff = new byte[capacity];
                System.arraycopy(pcmBuffer, aacFrameSize, lBuff, 0, capacity);
                System.arraycopy(lBuff, 0, pcmBuffer, 0, capacity);
            }
    
            ret = avcodec_encode_audio2(pCodecCtx, pkt, aacFrame, got);
            if (ret < 0) {
                throw new FFmpegException(ret, "avcodec_encode_audio2 音频编码失败");
            }
            if (got[0] != 0) {
                byte[] pktBuff = new byte[pkt.size()];
                pkt.data().get(pktBuff);
                if (aacBuffConsumer != null) {
                    aacBuffConsumer.accept(pktBuff);
                }
                av_packet_unref(pkt);
            }
        }
    }
    

    最后只需要调整一下上一次的主程序,将读取pcm数据的部分,调整为将AVFrame丢进编码器,拉取byte数组即可。

    public static void main(String[] args) throws FFmpegException, FileNotFoundException {
        FFmpegRegister.register();
        AudioGrabber a = AudioGrabber.create("External Mic (Realtek(R) Audio)");
    
        FileOutputStream fos = new FileOutputStream(new File("s16.aac"));
        AudioAACEncoder encoder = AudioAACEncoder.create(a.channels(), a.sample_fmt(), a.sample_rate(), buff -> {
            try {
                fos.write(buff);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        for (int i = 0; i < 100; i++) {
            encoder.encode(a.grab());
        }
        encoder.release();
        a.release();
    }
    

    最终采集编码后的AAC数据可以用VLC播放:

    这里对比一下,同样的100帧pcm数据和aac数据的大小,相差还是很大的。

    =========================================================
    AAC编码源码可关注公众号 “HiIT青年” 发送 “ffmpeg-aac” 获取。

    HiIT青年
    关注公众号,阅读更多文章。

  • 相关阅读:
    软件测试常见概念
    Apollo简介及工作原理
    bug的编写技巧与级别划分
    native与H5优缺点及H5测试
    优惠券测试
    go语言-for循环
    go语言-流程控制--if
    go语言-二进制与位运算
    cookie和session
    AJAX
  • 原文地址:https://www.cnblogs.com/itqn/p/14225880.html
Copyright © 2011-2022 走看看