FFmpeg學習3:播放音頻

參考dranger tutorial,本文將介紹如何使用FFmpeg解碼音頻數據,並使用SDL將解碼後的數據輸出。
本文主要包含以下幾方面的內容:
* 關於播放音頻的需要的一些基礎知識介紹
* 使用SDL2播放音頻
* 數據隊列
* 音頻格式的轉換

dranger tutorial確實入門FFmpeg比較好的教程,雖然作者在2015年的時候根據新版本的FFmpeg更新了,

但是其中還是有不少API過時了。特別是,教程中使用的是SDL1.0,和現在的SDL2的API也有很大的不同,並且不能兼容。

1. 關於音頻的一些基礎知識

和視頻一樣,音頻數據也會被打包到一個容器中,其文件的擴展名並不是其編碼的方法,只是其打包文件的格式。
現實世界中,我們所聽到的聲音是一個個連續的波形,但是計算機無法存儲和處理這種擁有無限點的波形數據。所以通過重採樣,按照一定的
頻率(1秒採集多少個點),將有無限個點的連續波形數據轉換爲有有限個點的離散數據,這就是通常說的A/D轉換(模數轉換,將模擬數據轉換爲數字數據)。
通過上面轉換過程的描述可以知道,一個數字音頻通常由以下三個部分組成:

  • 採樣頻率 採樣是在擁有無限個點的連續波形上選取有限個離散點,採集到的離散點越多,也就越能真實的波形。由於聲音是在時間上的連續波形,其採樣點的間隔就是兩次採樣的時間間隔。通俗來說,採樣率指的是每秒鐘採樣的點數,單位爲HZ。採樣率的倒數是採樣週期,也就是兩次採樣的時間間隔。採樣率越大,則採集到的樣本點數就越多,採樣得到的數字音頻也就更接近真實的聲音波形,相應的其佔用的存儲空間也就越大。常用的採樣頻率有:

    • 22k Hz 無限廣播所用的採樣率
    • 44.1k Hz CD音質
    • 48k Hz 數字電視,DVD
    • 96k Hz 192k Hz 藍光盤,高清DVD
  • 採樣精度 採集到的點被稱爲樣本(sample),每個樣本佔用的位數就是採樣精度。這點和圖像的像素表示比較類似,可以使用8bit,16bit或者24bit來表示採集到的一個樣本。同樣,一個樣本佔用的空間越大其表示的就越接近真實的聲音。

  • 通道 支持不同發聲的音響的個數。常用的聲道有單聲道、雙聲道、4聲道、5.1聲道等。不同的聲道在採樣的時候是不同的,例如雙聲道,在每次採樣的時候有采集兩個樣本點。

下圖是一個三通道的音頻示例

  • 比特率 指的是每秒傳送的比特(bit)數,其單位是bps(Bit Per Second),是間接衡量聲音質量的一個標準。

    • 沒有壓縮編碼的音頻數據,其比特率 = 採樣頻率 * 採樣精度 * 通道數,通過該公式可以看出,比特率越高,採樣得到的聲音質量就越高,相應的佔用的存儲空間也就越大。
    • 經過壓縮編碼後的音頻數據也有一個比特率,這時候的比特率也可以稱之爲碼率,因爲其反映了壓縮編碼的效率。碼率越高,壓縮後的數據越大,得到音頻質量越好,相應的壓縮的效率也就越低。
      碼率 = 音頻文件的大小 / 時長,在時長一定的情況下,碼率越高則音頻文件越大,其音頻的品質也就越高。常見的一些碼率:

      • 96 Kbps FM質量
      • 128 - 160 Kbps 比較好的音頻質量
      • 192Kbps CD質量
      • 256Kbps 320Kbps 高質量音頻

通常我們所說的比特率(碼率)指的是編碼後的每秒鐘的數據量。碼率越高,壓縮比就越小,音頻文件就越大,相對的音頻質量也就越好。碼率可以反映出音頻的質量,碼率越高,音頻質量就越,反之亦然。

2.SDL2播放音頻

使用SDL播放解碼後的音頻數據,SDL播放音頻數據的流程如下:

使用SDL播放聲音前,首先要設置一些關於音頻的選項:採樣率,通道數,採樣精度,然後還要指定一個回調函數callback以及用戶數據(在播放時需要用到的數據指針)。播放音頻的時候,SDL將調用回調函數將待播放的音頻數據填充到一個特定的緩衝區中。

2.1 SDL_AudioSpec

SDL中的結構體SDL_AudioSpec包含了關於音頻的格式信息(採樣率,通道數,採樣精度)和回調函數以及用戶數據指針,其聲明如下:

typedef struct SDL_AudioSpec
{
    int freq;                   /**< DSP frequency -- samples per second */
    SDL_AudioFormat format;     /**< Audio data format */
    Uint8 channels;             /**< Number of channels: 1 mono, 2 stereo */
    Uint8 silence;              /**< Audio buffer silence value (calculated) */
    Uint16 samples;             /**< Audio buffer size in samples (power of 2) */
    Uint16 padding;             /**< Necessary for some compile environments */
    Uint32 size;                /**< Audio buffer size in bytes (calculated) */
    SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
    void *userdata;             /**< Userdata passed to callback (ignored for NULL callbacks). */
} SDL_AudioSpec;  
  • freq 每秒鐘發送給音頻設備的sample frame的個數,通常是11025,220502,44100和48000。(sample frame = 樣本精度 * 通道數)
  • fromat 每個樣本佔用的空間大小及格式,例如 AUDIO_S16SYS,樣本是有符號的16位整數,字節順序(大端還是小端)和系統一樣。更多的格式可參考SDL_AudioFormat。
  • channels 通道數,在SDL2.0中支持1(mono),2(stereo),4(quad)和6(5.1)
  • samples 緩衝區的大小( sample frame爲單位)。
  • silence 音頻數據中表示靜音的值是多少
  • size 緩衝區的大小(字節爲單位)
  • callback 用來音頻設備緩衝區的回調函數
  • userdata 在回調函數中使用的數據指針
2.2 回調函數

callback用來取音頻數據給音頻設備,其聲明如下:

void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,int len)  ;
  • userdata 就是SDL_AudioSpec結構體中的userdata字段
  • stream 要填充的緩衝區
  • len 緩衝區的大小

在回調函數內要首先初始化緩衝區stream,並且在回調函數返回結束後,緩衝區就不再可用了。對於多聲道的音頻數據在緩衝區中是交錯存放的
* 雙聲道 LRLR
* 四聲道 front-left / front-right / rear-left /rear-right
* 5.1 front-left / front-right / center / low-freq / rear-left / rear-right

2.3 打開音頻設備

在設置後需要參數(填充好SDL_AudioSpec的各個字段)後,下面要做的就是打開音頻設備。函數SDL_OpenAudioDevice用來打開聲音設備,其聲明如下:

SDL_OpenAudioDevice(const char  *device,int iscapture,const SDL_AudioSpec *desired, SDL_AudioSpec *obtained,int allowed_changes);  
  • 該函數需要的第一個參數是音頻設備的名稱,調用SDL_GetAudioDeviceName根據設備的編號獲取到設備的名稱。下面這段代碼用來輸出所有的設備名稱:
    int count = SDL_GetNumAudioDevices(0);
    for (int i = 0; i < count; i++)
    {
        cout << "Audio device " << i << " : " << SDL_GetAudioDeviceName(i, 0);
    }  
  • iscapture 設爲0,非0的值在當前SDL2版本還不支持
  • desired 期望得到的音頻輸出格式
  • obtained 實際的輸出格式
  • allowed_changes 期望和實際總是有差別,該參數用來指定 當期望和實際的不一樣時,能不能夠對某一些輸出參數進行修改。
    設爲0,則不能修改。設爲如下的值,則可對相應的參數修改:
    • SDL_AUDIO_ALLOW_FREQUENCY_CHANGE
    • SDL_AUDIO_ALLOW_FORMAT_CHANGE
    • SDL_AUDIO_ALLOW_CHANNELS_CHANGE
    • SDL_AUDIO_ALLOW_ANY_CHANGE

調用 SDL_OpenAudioDevice打開音頻設備後,就會爲callback函數單獨開啓一個線程,不斷的將音頻發送的音頻設備進行播放.

3. 數據隊列

上面已經設定音頻輸出的格式,打開了音頻設備,並且開啓了傳送音頻數據的線程(callback函數),就等着FFmpeg解碼的音頻數據了。
在介紹音頻數據的組織之前,先來看下在本文中音頻的播放的整個流程,如下圖所示:

前期的準備工作,如打開音頻文件,查找音頻流,找到音頻解碼器等過程,和視頻類似,這裏不再贅述。
從上圖可知,首先不再對從音頻流中讀取到的AvPacket進行解碼,而是將其緩存到一個AVPacket隊列中。當callback函數從緩衝區中
取數據發送到音頻設備時,從AVPacket隊列中取出Packet,解碼後填充到緩衝區。
所以首先要做的就是創建一個AVPacket隊列,可以使用FFmpeg中的AVPacketList作爲隊列的一個節點,隊列的聲明如下:

typedef struct PacketQueue
{
    AVPacketList *first_pkt; // 隊頭
    AVPacketList *last_pkt; // 隊尾
    int nb_packets; //包的個數
    int size; // 佔用空間的字節數
    SDL_mutex* mutext; // 互斥信號量
    SDL_cond* cond; // 條件變量
}PacketQueue;  

注意,AVPacket隊列實際會有兩個線程訪問:主線程和callback函數,所以這裏聲明瞭一個互斥信號量來保證對隊列的正確讀寫,
以及一個條件變量進行線程的同步操作。
對隊列的操作有下面兩個方法:
* packet_queue_put 向隊尾插入一個Packet

    SDL_LockMutex(q->mutext);
    if (!q->last_pkt) // 隊列爲空,新插入元素爲第一個元素
        q->first_pkt = pktl;
    else // 插入隊尾
        q->last_pkt->next = pktl;
    q->last_pkt = pktl;
    q->nb_packets++;
    q->size += pkt->size;
    SDL_CondSignal(q->cond);
    SDL_UnlockMutex(q->mutext);  

在向隊尾插入元素時要先進行lock操作,插入完成SDL_CondSignal(q->cond)通知取數據的線程,已有新插入的數據。
* packet_queue_get,取出隊首Packet。由於向隊列的插入數據實在主線程中完成的,而取數據則是在callback線程中進行的,所以有可能在
取數據的時候,隊列爲空。在packet_queue_get中有一個block參數,指定在無數據的時候是否阻塞線程等待。

    while (true)
    {
        pktl = q->first_pkt;
        if (pktl)
        {
            q->first_pkt = pktl->next;
            if (!q->first_pkt)
                q->last_pkt = nullptr;
            q->nb_packets--;
            q->size -= pktl->pkt.size;
            *pkt = pktl->pkt;
            av_free(pktl);
            ret = 1;
            break;
        }
        else if (!block)
        {
            ret = 0;
            break;
        }
        else
        {
            SDL_CondWait(q->cond, q->mutext);
        }
    }  

在隊列無數據的時候,設定爲阻塞等待則SDL_CondWait(q->cond, q->mutext)等待,不然就直接返回。

4. 解碼音頻及其格式轉換

已經準備好了Packet隊列,下面要做的就是從 Audio File中取出Packet並填充到Packet Queue中,然後在callback函數中對Packet進行
解碼,並將解碼後的數據傳送到音頻設備播放。

4.1 插入Packet
    packet_queue_init(&audioq);
    SDL_PauseAudio(0);
    AVPacket packet;
    while (av_read_frame(pFormatCtx, &packet) >= 0)
    {
        if (packet.stream_index == audioStream)
            packet_queue_put(&audioq, &packet);
        else
            av_free_packet(&packet);
    }  

首先初始化PacketQueue,SDL_PauseAudio是讓音頻設備開始播放,如果沒有提供數據,則播放靜音。接着一個while循環將,從音頻
流中提取Packet,放入到隊列中。插入到隊列中的Packet沒有被free,等到packet被解碼後,纔會被free掉。

4.2 取出Packet並解碼

Packet已經放入到隊列中,接下來就要在callback函數中,取出packet並解碼傳送到音頻設備。


void audio_callback(void* userdata, Uint8* stream, int len)
{
    AVCodecContext* aCodecCtx = (AVCodecContext*)userdata;
    int len1, audio_size;
    static uint8_t audio_buff[(MAX_AUDIO_FRAME_SIZE * 3) / 2];
    static unsigned int audio_buf_size = 0;
    static unsigned int audio_buf_index = 0;
    SDL_memset(stream, 0, len);
    while (len > 0)
    {
        if (audio_buf_index >= audio_buf_size)
        {
            audio_size = audio_decode_frame(aCodecCtx, audio_buff, sizeof(audio_buff));
            if (audio_size < 0)
            {
                audio_buf_size = 1024;
                memset(audio_buff, 0, audio_buf_size);
            }
            else
                audio_buf_size = audio_size;
            audio_buf_index = 0;
        }
        len1 = audio_buf_size - audio_buf_index;
        if (len1 > len)
            len1 = len;
        memcpy(stream, (uint8_t*)(audio_buff + audio_buf_index), audio_buf_size);
        len -= len1;
        stream += len1;
        audio_buf_index += len1;
    }
}  

在callback函數中,設置了一個靜態的數據緩衝區 static uint8_t audio_buff[(MAX_AUDIO_FRAME_SIZE * 3) / 2],callback函數每次向音頻設備傳送數據時,首先檢測靜態緩存區中是否有數據,有則直接複製到stream中memcpy(stream, (uint8_t*)(audio_buff + audio_buf_index), audio_buf_size);;否則調用audio_decode_frame函數,從Packet 隊列中取出Packet,將解碼後的數據填充到靜態緩衝區,然後再傳送到音頻設備。

4.3 解碼數據並進行格式轉換

在callback函數中調用audio_decode_frame函數,解碼Packet隊列中的Packet。audio_decode_frame聲明如下:

int audio_decode_frame(AVCodecContext* aCodecCtx, uint8_t* audio_buf, int buf_size)  

audio_buf是callback函數中定義的靜態緩存空間,該函數將解碼後的數據填充到該空間。

int got_frame = 0;
len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
if (len1 < 0) // 出錯,跳過
{
    audio_pkt_size = 0;
    break;
}
audio_pkt_data += len1;
audio_pkt_size -= len1;
data_size = 0;
if (got_frame)
{
    data_size = av_samples_get_buffer_size(nullptr, aCodecCtx->channels, frame.nb_samples, aCodecCtx->sample_fmt, 1);
    assert(data_size <= buf_size);
    memcpy(audio_buf, frame.data[0], data_size);
}  

得到解碼後的數據後,還有一個問題就是,輸入的音頻格式可能和SDL打開的音頻設備所支持的格式不一致,所以在將數據放入到callback的靜態緩衝區前,需要做一個數據格式的轉換。
首先創建一個SwrContext,並設定好轉換參數,並初始化該SwrContext

SwrContext* swr_ctx = nullptr;
if (swr_ctx)
{
    swr_free(&swr_ctx);
    swr_ctx = nullptr;
}

swr_ctx = swr_alloc_set_opts(nullptr, wanted_frame.channel_layout, (AVSampleFormat)wanted_frame.format, wanted_frame.sample_rate,
    frame.channel_layout, (AVSampleFormat)frame.format, frame.sample_rate, 0, nullptr);
if (!swr_ctx || swr_init(swr_ctx) < 0)
{
    cout << "swr_init failed:" << endl;
    break;
}

設定的swr選項有三個:數據佈局channel_layout,樣本的精度format和每秒的sample個數sample_rate,需要設定轉換前後的這三個參數。

int dst_nb_samples = av_rescale_rnd(swr_get_delay(swr_ctx, frame.sample_rate) + frame.nb_samples,
    wanted_frame.sample_rate, wanted_frame.format, AVRounding(1));
int len2 = swr_convert(swr_ctx, &audio_buf, dst_nb_samples,
    (const uint8_t**)frame.data, frame.nb_samples);

參數設定好後,在進行轉換前,還需要計算出轉換後的sample的個數,上面代碼中的dst_nb_samples。然後調用swr_convert就完成了輸入的音頻到輸出音頻格式的轉換

5 總結

本文主要參考dranger tutorial的播放音頻教程,對使用FFmpeg + SDL播放音頻做了個總結。主要以下內容:

  • 關於音頻的一些基礎知識:採樣率,通道,比特率等
  • SDL2播放音頻的流程
  • 使用Packet隊列,FFmpeg解碼
    在FFmpeg解碼,然後發送給音頻設備的時候要進行音頻格式的轉換,不然在播放的時候有可能有雜音。

接下來打算對AVPacket這個結構體進行個分析,在總結的時候發現AVPacket中的數據緩存管理還是有點複雜的。而且,在本文代碼中使用的av_dup_packetav_packet_free已經廢棄了。

/**
 * Free a packet.
 *
 * @deprecated Use av_packet_unref
 *
 * @param pkt packet to free
 */
attribute_deprecated
void av_free_packet(AVPacket *pkt); 

/**
 * @warning This is a hack - the packet memory allocation stuff is broken. The
 * packet is allocated if it was not really allocated.
 *
 * @deprecated Use av_packet_ref
 */
attribute_deprecated
int av_dup_packet(AVPacket *pkt); 

本文代碼FFmpeg3.cpp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章