1 音頻編碼
本質上是由pcm文件轉到一個協議文件 比如說aac協議
1.1 音頻基本知識回歸
- 比特率
比特率是指單位時間內傳輸或處理的比特(bit)數量,通常用 bps(bits per second,比特每秒)來表示。它是衡量數據傳輸速度和通信系統能力的重要指標。 - 碼率
一般為編碼之后,單位時間內傳輸的字節數 - 聲道布局
聲道布局是指在音頻系統中,各個聲道的揚聲器在空間中的分布方式以及它們所承擔的音頻信息分配方式。以下是一些常見的聲道布局簡介:
單聲道:只有一個聲道,所有音頻信息都通過一個揚聲器播放,聲音缺乏空間感和立體感,常用于一些簡單的音頻設備或對音頻要求不高的場景。
立體聲:有左聲道和右聲道兩個聲道,通過兩個揚聲器分別播放不同的音頻信號,模擬人耳聽到聲音的立體感,能讓聽眾感受到聲音的左右位置差異,營造出一定的空間感,是音樂播放、電影音頻等常見的聲道布局。
5.1 聲道:由左前、中置、右前、左環繞、右環繞五個揚聲器以及一個重低音揚聲器(.1 表示)組成。左前、中置和右前揚聲器主要負責前方的聲音,包括人物對話、主要樂器聲等;左環繞和右環繞揚聲器用于營造環境音效和后方聲音,增強沉浸感;重低音揚聲器專門負責低頻音效,如爆炸聲、雷聲等,能提升聲音的震撼力,常用于家庭影院系統。
7.1 聲道:在 5.1 聲道的基礎上,增加了左后環繞和右后環繞兩個聲道,進一步增強了后方的聲音細節和空間感,使聽眾能更清晰地感受到來自后方各個方向的聲音,提供更逼真的環繞音效,常用于高端影院和一些專業音頻制作環境。
- 采樣率
采樣率是指在單位時間內對模擬信號進行采樣的次數,通常用赫茲(Hz)來表示 - 采樣格式
即一個樣本點多少位存放
packed格式:交錯
1 AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
2 AV_SAMPLE_FMT_S16, ///< signed 16 bits
3 AV_SAMPLE_FMT_S32, ///< signed 32 bits
4 AV_SAMPLE_FMT_FLT, ///< float
5 AV_SAMPLE_FMT_DBL, ///< double
planar格式 左右分別存放
1 AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
2 AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
3 AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
4 AV_SAMPLE_FMT_FLTP, ///< float, planar
5 AV_SAMPLE_FMT_DBLP, ///< double, planar
6 AV_SAMPLE_FMT_S64, ///< signed 64 bits
7 AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
- 一幀大小
計算出每一幀的數據 單個采樣點的字節 * 通道數目 * 每幀采樣點數量 - aac協議
aac協議由一個adts頭和一個音頻數據包組成 一般希望planar格式 - 編碼器選擇
ffmpeg默認的aac編碼器,默認編譯出來的每幀數據都不帶adts,但lib_fdk aac默認是帶了adts header,而且此時codec_ctx->flags的值都為0.
這樣我們沒法判斷是否需要自己額外寫入adts,因此我們在設置編碼的時候可以直接將codec_ctx->flags = AV_CODEC_FLAG_GLOBAL_HEADER; 大家都不帶adts
2.2 編碼流程
const AVCodec *codec = NULL;//編碼器
AVCodecContext *codec_ctx= NULL;//編碼器設置codec = avcodec_find_encoder(codec_id)
codec_ctx = avcodec_alloc_context3(codec)codec_ctx->codec_id = codec_id;//idcodec_ctx->codec_type = AVMEDIA_TYPE_AUDIO;//typecodec_ctx->bit_rate = 128*1024;//碼率codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;//立體聲音codec_ctx->sample_rate = 48000; //48000 采樣率codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);codec_ctx->profile = FF_PROFILE_AAC_LOW; //低水平if(strcmp(codec->name, "aac") == 0) {codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;} else if(strcmp(codec->name, "libfdk_aac") == 0) {codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;} else {codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;}//采樣格式/* 檢測支持采樣格式支持情況 */if (!check_sample_fmt(codec, codec_ctx->sample_fmt)) {fprintf(stderr, "Encoder does not support sample format %s",av_get_sample_fmt_name(codec_ctx->sample_fmt));exit(1);}if (!check_sample_rate(codec, codec_ctx->sample_rate)) {fprintf(stderr, "Encoder does not support sample rate %d", codec_ctx->sample_rate);exit(1);}if (!check_channel_layout(codec, codec_ctx->channel_layout)) {fprintf(stderr, "Encoder does not support channel layout %lu", codec_ctx->channel_layout);exit(1);}codec_ctx->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {fprintf(stderr, "Could not open codec\n");exit(1);}
這一部分 無疑完成編碼器的初始化
先找編碼器->再初始化上下文->再連接再一起
note:不同的編碼器對采樣格式有性能限制,aac編碼器傾向于planar格式編碼,libfdk_aac傾向于packed格式編碼
AVFrame *frame = NULL;//為原始數據
AVPacket *pkt = NULL;//編碼數據
pkt = av_packet_alloc();
frame = av_frame_alloc();frame->nb_samples = codec_ctx->frame_size;//一幀多少采樣點frame->format = codec_ctx->sample_fmt;//音頻深度frame->channel_layout = codec_ctx->channel_layout;frame->channels = av_get_channel_layout_nb_channels(frame->channel_layout);ret = av_frame_get_buffer(frame, 0);int frame_bytes = av_get_bytes_per_sample(frame->format) * frame->channels * frame->nb_samples;uint8_t *pcm_buf = (uint8_t *)malloc(frame_bytes);
size_t read_bytes = fread(pcm_buf, 1, frame_bytes, infile);ret = av_frame_make_writable(frame);ret = av_samples_fill_arrays(frame->data, frame->linesize,pcm_buf, frame->channels,frame->nb_samples, frame->format, 0);
分配幀和數據包->初始化幀(給幀分配大小)->填充幀
ret = av_frame_get_buffer(frame, 0);0為讓ffmpeg選擇 是否對齊
av_samples_fill_arrays
uint8_t **audio_data
作用:這是一個指向指針數組的指針,用于存儲每個聲道的音頻數據指針。AVFrame 中的 data 成員通常就是這樣一個指針數組,用于存儲不同聲道的數據。例如,對于立體聲(2 個聲道),audio_data[0] 指向左聲道數據,audio_data[1] 指向右聲道數據。
示例:在 AVFrame 中使用時,通常傳入 frame->data。
2. int *linesize
作用:這是一個指向整數數組的指針,用于存儲每個聲道數據的每行字節數(在音頻數據中,“行” 可以理解為一個樣本塊)。這個值對于音頻數據的正確處理非常重要,因為它表示了每個聲道數據在內存中的布局信息。AVFrame 中的 linesize 成員就是用于存儲這些信息的。
示例:在 AVFrame 中使用時,通常傳入 frame->linesize。
3. const uint8_t *buf
作用:這是一個指向包含原始音頻樣本數據的緩沖區的指針。該緩沖區包含了要填充到 AVFrame 中的音頻數據。
示例:在你的代碼中,pcm_temp_buf 就是存儲原始 PCM 音頻數據的緩沖區,所以這里傳入 pcm_temp_buf。
4. int nb_channels
作用:表示音頻數據的聲道數。例如,單聲道音頻的 nb_channels 為 1,立體聲音頻的 nb_channels 為 2。
示例:在 AVFrame 中使用時,通常傳入 frame->channels。
?5. int nb_samples
作用:表示每個聲道的樣本數量。樣本是音頻數據的基本單位,nb_samples 決定了音頻數據的時長和大小。
示例:在 AVFrame 中使用時,通常傳入 frame->nb_samples。
6. enum AVSampleFormat sample_fmt
作用:表示音頻樣本的格式,它指定了每個樣本的位深度、存儲方式等信息。常見的樣本格式有 AV_SAMPLE_FMT_S16(16 位有符號整數)、AV_SAMPLE_FMT_FLTP(浮點型平面格式)等。
示例:在 AVFrame 中使用時,通常傳入 frame->format。
7. int align
作用:指定內存對齊方式。內存對齊可以提高內存訪問效率,通常傳入 0 讓 FFmpeg 自動選擇合適的對齊方式。
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *output)
{int ret;/* send the frame for encoding */ret = avcodec_send_frame(ctx, frame);if (ret < 0) {fprintf(stderr, "Error sending the frame to the encoder\n");return -1;}/* read all the available output packets (in general there may be any number of them */// 編碼和解碼都是一樣的,都是send 1次,然后receive多次, 直到AVERROR(EAGAIN)或者AVERROR_EOFwhile (ret >= 0) {ret = avcodec_receive_packet(ctx, pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;} else if (ret < 0) {fprintf(stderr, "Error encoding audio frame\n");return -1;}size_t len = 0;printf("ctx->flags:0x%x & AV_CODEC_FLAG_GLOBAL_HEADER:0x%x, name:%s\n",ctx->flags, ctx->flags & AV_CODEC_FLAG_GLOBAL_HEADER, ctx->codec->name);if((ctx->flags & AV_CODEC_FLAG_GLOBAL_HEADER)) {// 需要額外的adts header寫入uint8_t aac_header[7];get_adts_header(ctx, aac_header, pkt->size);len = fwrite(aac_header, 1, 7, output);if(len != 7) {fprintf(stderr, "fwrite aac_header failed\n");return -1;}}len = fwrite(pkt->data, 1, pkt->size, output);if(len != pkt->size) {fprintf(stderr, "fwrite aac data failed\n");return -1;}
}return -1;
}
發送幀->接受幀->寫入文件
引入一個函數
2 視頻編碼
本質上是將一個yuv 變成一個h264文件
2.1 h264編碼細節推薦
1 什么是視頻碼率
視頻碼率是視頻數據(包含視頻?彩量、亮度量、像素量)每秒輸出的位數。?般?的單位是kbps。
2 設置視頻碼率的必要性
在?絡視頻應?中,視頻質量和?絡帶寬占?是相?盾的。通常情況下,視頻流占?的帶寬越?則視頻
質量也越?,需要的?絡帶寬也越?,解決這??盾的鑰匙當然是視頻編解碼技術。評判?種視頻編解碼技術的優劣,是?較在相同的帶寬條件下,哪個視頻質量更好;在相同的視頻質量條件下,哪個占?的?絡帶寬更少(?件體積?)。
是不是視頻碼率越?,質量越好呢?理論上是這樣的。然?在我們?眼分辨的范圍內,當碼率?到?定程度時,就沒有什么差別了。所以碼率設置有它的最優值,H.264(也叫AVC或X264)的?件中,視頻的建議碼率如下
3 –preset的參數主要調節編碼速度和質量的平衡,有ultrafast、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo這10個選項,從快到慢。
4 –tune的參數主要配合視頻類型和視覺優化的參數,或特別的情況。如果視頻的內容符合其中一個可用的調整值又或者有其中需要,則可以使用此選項,否則建議不使用(如tune grain是為高比特率的編碼而設計的)。tune的值有:
film: 電影、真人類型;
animation: 動畫;
grain: 需要保留大量的grain時用;
stillimage: 靜態圖像編碼時使用;
psnr: 為提高psnr做了優化的參數;
ssim: 為提高ssim做了優化的參數;
fastdecode: 可以快速解碼的參數;
zerolatency:零延遲,用在需要非常低的延遲的情況下,比如電視電話會議的編碼。
5 在 H.264 編碼中,profile(配置文件)用于指定編碼視頻流的特性和功能集合,不同的profile適用于不同的應用場景和設備。常見的 H.264 profile值及其特點如下:
- Baseline Profile(基線配置文件)
支持 I 幀(關鍵幀)和 P 幀(預測幀),不支持 B 幀(雙向預測幀)。
主要用于低延遲、低復雜度的應用,如視頻會議、實時監控等。
能夠在相對較低的碼率下提供較好的視頻質量,適用于帶寬有限的情況。 - Main Profile(主配置文件)
支持 I 幀、P 幀和 B 幀,提供了更高效的壓縮性能。
適用于大多數傳統的視頻應用,如視頻存儲、視頻廣播等。
在相同的視頻質量下,Main Profile 通常比 Baseline Profile 需要更高的碼率,但能實現更好的壓縮效果。 - High Profile(高配置文件)
進一步提高了壓縮效率,支持更多的編碼工具和特性,如更多的量化矩陣、自適應環路濾波等。
主要用于高清和藍光視頻等對視頻質量要求較高的應用。
能夠在高分辨率和高幀率的情況下提供出色的視頻質量,但編碼和解碼的復雜度也相對較高。
baseline profile多應?于實時通信領域;
main profile多應?于流媒體領域;
high profile則多應?于?電和存儲領域
6 bframes: I幀和P幀或者兩個P幀之間可?的最?連續B幀數量,默認值為3。B幀可使?雙向預測,從?顯著提?壓縮率,但由于需要緩存更多的幀數以及重排序的原因,會降低編碼速度,增加編碼延遲,因此在實時編碼時也建議將該值設置為0。
const AVCodec *codec = NULL;
AVCodecContext *codec_ctx= NULL;codec = avcodec_find_encoder_by_name(codec_name);if (!codec) {fprintf(stderr, "Codec '%s' not found\n", codec_name);exit(1);}codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx) {fprintf(stderr, "Could not allocate video codec context\n");exit(1);}
codec_ctx->width = 1280;
codec_ctx->height = 720;
codec_ctx->time_base = (AVRational){1, 25};
codec_ctx->framerate = (AVRational){25, 1};
codec_ctx->gop_size = 25; // I幀間隔
codec_ctx->max_b_frames = 2; // 如果不想包含B幀則設置為0
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;* // ultrafast all encode time:2270ms// medium all encode time:5815ms// veryslow all encode time:19836ms*/ret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);if(ret != 0) {printf("av_opt_set preset failed\n");}/*profile 參數:profile 表示 H.264 編碼的配置文件,這里設置為 main。* 不同的 profile 支持不同的編碼特性和功能,main 配置文件提供了基本的編碼功能,適用于大多數視頻應用場景。*/ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0); // 默認是highif(ret != 0) {printf("av_opt_set profile failed\n");}/*tune 參數:tune 表示針對特定應用場景進行優化,* 這里設置為 zerolatency,表示進行零延遲編碼,適用于直播等對延遲要求較高的場景。*/ret = av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0);codec_ctx->bit_rate = 3000000;ret = avcodec_open2(codec_ctx, codec, NULL);if (ret < 0) {fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));exit(1);}
同理 先找編碼器->初始化上下文->再綁定
AVFrame *frame = NULL;
AVPacket *pkt = NULL;pkt = av_packet_alloc();if (!pkt) {fprintf(stderr, "Could not allocate video frame\n");exit(1);}frame = av_frame_alloc();if (!frame) {fprintf(stderr, "Could not allocate video frame\n");exit(1);}// 為frame分配bufferframe->format = codec_ctx->pix_fmt;frame->width = codec_ctx->width;frame->height = codec_ctx->height;ret = av_frame_get_buffer(frame, 0);int frame_bytes = av_image_get_buffer_size(frame->format, frame->width,frame->height, 1);
int8_t *yuv_buf = (uint8_t *)malloc(frame_bytes);ret = av_frame_make_writable(frame);int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,frame->format,frame->width, frame->height, 1);
同理 設置幀參數
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,FILE *outfile)
{int ret;/* send the frame to the encoder */if (frame)printf("Send frame %3"PRId64"\n", frame->pts);/* 通過查閱代碼,使用x264進行編碼時,具體緩存幀是在x264源碼進行,* 不會增加avframe對應buffer的reference*/ret = avcodec_send_frame(enc_ctx, frame);if (ret < 0){fprintf(stderr, "Error sending a frame for encoding\n");return -1;}while (ret >= 0){ret = avcodec_receive_packet(enc_ctx, pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;} else if (ret < 0) {fprintf(stderr, "Error encoding audio frame\n");return -1;}if(pkt->flags & AV_PKT_FLAG_KEY)//該數據包是否為關鍵幀printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",pkt->flags, pkt->pts, pkt->dts, pkt->size);if(!pkt->flags)printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",pkt->flags, pkt->pts, pkt->dts, pkt->size);fwrite(pkt->data, 1, pkt->size, outfile);}return 0;
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr);
此函數把輸入的 AVFrame 編碼成一個或多個 AVPacket,使用單個函數調用完成編碼過程。got_packet_ptr 用于指示是否生成了有效的數據包。
avcodec_encode_video2:是一個同步接口,在一次調用里完成幀的發送和編碼后數據包的接收。若編碼器有緩存幀,可能需要多次調用該函數才能處理完所有緩存幀。
3 共同點
1.為什么要沖刷編碼器
- 清空緩存:編碼器通常會有內部緩存來存儲尚未輸出的編碼數據。在編碼結束時或某些特定情況下,如切換編碼參數、結束編碼流程等,需要沖刷編碼器,以確保這些緩存中的數據被完整地輸出,避免數據丟失。
- 保證數據完整性:沖刷操作可以使編碼器將所有已處理但未發送的編碼數據包發送出來,從而保證編碼后的數據在文件或流中的完整性,使得后續的解碼和播放等操作能夠順利進行。
- 避免延遲:如果不進行沖刷,緩存中的數據可能會一直滯留在編碼器中,導致輸出延遲。沖刷編碼器能夠及時將數據輸出,降低整體的編碼延遲,特別是在實時性要求較高的場景,如直播、視頻會議等中,這一點尤為重要。
- 資源釋放與重置:沖刷編碼器有時也與資源的釋放和重置相關。在完成一次編碼任務后,通過沖刷可以清理編碼器內部的臨時資源,為下一次編碼任務做好準備,確保編碼器處于正確的初始狀態,避免殘留數據或狀態對新的編碼過程產生干擾。
2 為什么不要取消計數引用
avcodec_receive_packet 函數內部第一個調用的就是 av_packet_unref,它會對 AVPacket 進行引用計數的遞減操作,并在引用計數為 0 時釋放相關的資源。如果在外部再次手動調用 av_packet_unref,就會導致對同一個 AVPacket 的引用計數錯誤地多減一次,可能會造成資源過早釋放,后續如果再使用該 AVPacket 相關的指針或數據,就會引發錯誤,比如出現野指針訪問、程序崩潰等問題。
3 如何放入隊列
由于編碼器會釋放數據,不能直接將原始的 pkt 插入到隊列中。正確的做法是新分配一個 AVPacket,然后使用 av_packet_move_ref 函數將原始 pkt 對應的緩沖區轉移到新分配的 AVPacket 中,再將新的 AVPacket 放入隊列。這樣做可以保證隊列中的 AVPacket 擁有獨立的緩沖區,不會因為編碼器釋放原始 pkt 的數據而導致隊列中的數據被破壞。在放入隊列時,還需要確保隊列的操作是線程安全的,以避免多線程環境下的競爭條件和數據不一致問題。
4 流程不同
即aac要加頭文件,以及編碼器設置參數不一樣 ,以及由兩個api av_image_fill_arrays和av_samples_fill_arrays 不同