參考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_packet
和av_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