JavaCV FFmpeg AAC編碼


上次成功通過FFmpeg采集麥克風的PCM數據,這次針對上一次的程序進行了改造,使用AAC編碼采集后的數據。

(傳送門) JavaCV FFmpeg采集麥克風PCM音頻數據

其他關於JavaCV的文章,可以通過下面的鏈接查看:
JavaCV-開發系列文章匯總篇(https://www.cnblogs.com/itqn/p/14696221.html)

采集麥克風數據是一個解碼過程,而將采集后的數據進行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青年
關注公眾號,閱讀更多文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM