音視訊八股文(11)-- ffmpeg 音訊重取樣

2023-05-12 06:01:09

1重取樣

1.1 什麼是重取樣

所謂的重取樣,就是改變⾳頻的取樣率、sample format、聲道數等引數,使之按照我們期望的引數輸出。

1.2 為什麼要重取樣

為什麼要重取樣?當然是原有的⾳頻引數不滿⾜我們的需求,⽐如在FFmpeg解碼⾳頻的時候,不同的⾳源有不同的格式,取樣率等,在解碼後的資料中的這些引數也會不⼀致(最新FFmpeg 解碼⾳頻後,⾳頻格
式為AV_SAMPLE_FMT_FLTP,這個引數應該是⼀致的),如果我們接下來需要使⽤解碼後的⾳頻資料做其他操作,⽽這些引數的不⼀致導致會有很多額外⼯作,此時直接對其進⾏重取樣,獲取我們制定的⾳頻引數,這樣就會⽅便很多。

再⽐如在將⾳頻進⾏SDL播放時候,因為當前的SDL2.0不⽀持planar格式,也不⽀持浮點型的,⽽最新的FFMPEG 16年會將⾳頻解碼為AV_SAMPLE_FMT_FLTP格式,因此此時就需要我們對其重取樣,使之可以在SDL2.0上進⾏播放。

2 對應引數解析

2.1 取樣率

取樣裝置每秒抽取樣本的次數

2.2取樣格式及量化精度(位寬)

每種⾳頻格式有不同的量化精度(位寬),位數越多,表示值就越精確,聲⾳表現⾃然就越精準。FFMpeg中⾳頻格式有以下⼏種,每種格式有其佔⽤的位元組數資訊(libavutil/samplefmt.h):

enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16, ///< signed 16 bits
    AV_SAMPLE_FMT_S32, ///< signed 32 bits
    AV_SAMPLE_FMT_FLT, ///< float
    AV_SAMPLE_FMT_DBL, ///< double
    AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
    AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
    AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
    AV_SAMPLE_FMT_FLTP, ///< float, planar
    AV_SAMPLE_FMT_DBLP, ///< double, planar
    AV_SAMPLE_FMT_S64, ///< signed 64 bits
    AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
    AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
};

2.3 分⽚(plane)和打包(packed)

以雙聲道為例,帶P(plane)的資料格式在儲存時,其左聲道和右聲道的資料是分開儲存的,左聲道的資料儲存在data[0],右聲道的資料儲存在data[1],每個聲道的所佔⽤的位元組數為linesize[0]和linesize[1];

不帶P(packed)的⾳頻資料在儲存時,是按照LRLRLR...的格式交替儲存在data[0]中,linesize[0]表示總的資料量。

2.4 聲道分佈(channel_layout)

聲道分佈在FFmpeg\libavutil\channel_layout.h中有定義,⼀般來說⽤的⽐較多的是AV_CH_LAYOUT_STEREO(雙聲道)和AV_CH_LAYOUT_SURROUND(三聲道),這兩者的定義如下:

#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO | AV_CH_FRONT_CENTER)

2.5 ⾳頻幀的資料量計算

⼀幀⾳頻的資料量(位元組)=channel數 * nb_samples樣本數 * 每個樣本佔⽤的位元組數

如果該⾳頻幀是FLTP格式的PCM資料,包含1024個樣本,雙聲道,那麼該⾳頻幀包含的⾳頻資料量是210244=8192位元組。

AV_SAMPLE_FMT_DBL : 210248 = 16384

2.6 ⾳頻播放時間計算

以取樣率44100Hz來計算,每秒44100個sample,⽽正常⼀幀為1024個sample,可知每幀播放時間/1024=1000ms/44100,得到每幀播放時間=1024*1000/44100=23.2ms (更精確的是23.21995464852608)。

⼀幀播放時間(毫秒) = nb_samples樣本數 *1000/取樣率 =

(1)1024*1000/44100=23.21995464852608ms ->約等於 23.2ms,精度損失了0.011995464852608ms,如果累計10萬幀,誤差>1199毫秒,如果有視訊⼀起的就會有⾳視訊同步的問題。 如果按著23.2去計算pts(0 23.2 46.4 )就會有累積誤差。

(2)1024*1000/48000=21.33333333333333ms

3 FFmpeg重取樣API

分配⾳頻重取樣的上下⽂

struct SwrContext *swr_alloc(void);

當設定好相關的引數後,使⽤此函數來初始化SwrContext結構體

int swr_init(struct SwrContext *s);

分配SwrContext並設定/重置常⽤的引數。

struct SwrContext* swr_alloc_set_opts(struct SwrContext* s, // ⾳頻重取樣上下⽂
    int64_t out_ch_layout, // 輸出的layout, 如:5.1聲道
    enum AVSampleFormat out_sample_fmt, // 輸出的取樣格式。Float, S16,⼀般選⽤是s16 絕⼤部分音效卡⽀持
    int out_sample_rate, //輸出取樣率
    int64_t in_ch_layout, // 輸⼊的layout
    enum AVSampleFormat in_sample_fmt, // 輸⼊的取樣格式
    int in_sample_rate, // 輸⼊的取樣率
    int log_offset, // ⽇志相關,不⽤管先,直接為0
    void* log_ctx // ⽇志相關,不⽤管先,直接為NULL
);

將輸⼊的⾳頻按照定義的引數進⾏轉換並輸出

int swr_convert(struct SwrContext* s, // ⾳頻重取樣的上下⽂
    uint8_t** out, // 輸出的指標。傳遞的輸出的陣列
    int out_count, //輸出的樣本數量,不是位元組數。單通道的樣本數量。
    const uint8_t** in, //輸⼊的陣列,AVFrame解碼出來的DATA
    int in_count // 輸⼊的單通道的樣本數量。
);

in和in_count可以設定為0,以最後重新整理最後⼏個樣本。

釋放掉SwrContext結構體並將此結構體置為NULL;

void swr_free(struct SwrContext **s);

⾳頻重取樣,取樣格式轉換和混合庫。與lswr的互動是通過SwrContext完成的,SwrContext被分配給swr_alloc()或
swr_alloc_set_opts()。 它是不透明的,所以所有引數必須使⽤AVOptions API設定。為了使⽤lswr,你需要做的第⼀件事就是分配SwrContext。 這可以使⽤swr_alloc()或 swr_alloc_set_opts()來完成。 如果您使⽤前者,則必須通過AVOptions API設定選項。 後⼀個函數提供了相同的功能,但它允許您在同⼀語句中設定⼀些常⽤選項。

例如,以下程式碼將設定從平⾯浮動樣本格式到交織的帶符號16位元整數的轉換,從48kHz到44.1kHz的下采
樣,以及從5.1聲道到⽴體聲的下混合(使⽤預設混合矩陣)。 這是使⽤swr_alloc()函數。

SwrContext * swr = swr_alloc();
av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_5POINT1, 0);
av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
av_opt_set_int(swr, "in_sample_rate", 48000, 0);
av_opt_set_int(swr, "out_sample_rate", 44100, 0);
av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);

同樣的⼯作也可以使⽤swr_alloc_set_opts():

SwrContext * swr = swr_alloc_set_opts(NULL, // we're allocating a new context
    AV_CH_LAYOUT_STEREO, // out_ch_layout
    AV_SAMPLE_FMT_S16, // out_sample_fmt
    44100, // out_sample_rate
    AV_CH_LAYOUT_5POINT1, // in_ch_layout
    AV_SAMPLE_FMT_FLTP, // in_sample_fmt
    48000, // in_sample_rate
    0, // log_offset
    NULL); // log_ctx

⼀旦設定了所有值,它必須⽤swr_init()初始化。 如果需要更改轉換引數,可以使⽤AVOptions來更改引數,如上⾯第⼀個例⼦所述; 或者使⽤swr_alloc_set_opts(),但是第⼀個引數是分配的上下⽂。 您必須再次調⽤swr_init()。⼀旦設定了所有值,它必須⽤swr_init()初始化。 如果需要更改轉換引數,可以使⽤AVOptions來更改引數,如上⾯第⼀個例⼦所述; 或者使⽤swr_alloc_set_opts(),但是第⼀個引數是分配的上下⽂。 您必須再次調⽤swr_init()。
轉換本身通過重複調⽤swr_convert()來完成。 請注意,如果提供的輸出空間不⾜或取樣率轉換完成後,樣本可能會在swr中緩衝,這需要「未來」樣本。 可以隨時通過使⽤swr_convert()(in_count可以設定為0)來檢索不需要將來輸⼊的樣本。 在轉換結束時,可以通過調⽤具有NULL in和in incount的swr_convert()來重新整理重取樣緩衝區。

4 go程式碼

moonfdd/ffmpeg-go