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青年
    关注公众号,阅读更多文章。

  • 相关阅读:
    HDU 1025 Constructing Roads In JGShining's Kingdom (DP+二分)
    HDU 1158 Employment Planning
    HDU 2059 龟兔赛跑
    Csharp 简单操作Word模板文件
    Csharp windowform datagridview Clipboard TO EXCEL OR FROM EXCEL DATA 保存datagridview所有數據
    Csharp 讀寫文件內容搜索自動彈出 AutoCompleteMode
    Csharp windowform controls clear
    CSS DIV大图片右上角叠加小图片
    Csharp DataGridView自定义添加DateTimePicker控件日期列
    Csharp 打印Word文件默認打印機或選擇打印機設置代碼
  • 原文地址:https://www.cnblogs.com/itqn/p/14225880.html
Copyright © 2011-2022 走看看