上次成功通過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編碼的過程是解碼的逆過程,不過主線流程是類似的,如下圖:
基本上主要的步驟都是:
- 查詢編碼/解碼器
- 開啟編碼/解碼器
- 進行編碼/解碼
在FFmpeg的demo流程中其實還有建立流avformat_new_stream()
,寫入頭部資訊avformat_write_header()
和尾部資訊av_write_trailer()
等操作,這裡只是將PCM資料編碼成AAC,所以可以暫時不需要考慮這些操作。
將採集音訊流資料進行AAC編碼的整體流程主要有以下幾個步驟:
- 採集音訊幀
- 將視音訊幀重取樣
- 構建AAC編碼器
- 對音訊幀進行編碼
採集音訊幀
採集音訊流中的音訊幀在上一次採集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” 獲取。
關注公眾號,閱讀更多文章。