ffmpeg入門學習——文檔3:播放聲音

指導3:播放聲音

現在我們要來播放聲音。SDL也爲我們準備了輸出聲音的方法。函數SDL_OpenAudio()本身就是用來打開聲音設備的。它使用一個叫做SDL_AudioSpec結構體作爲參數,這個結構體中包含了我們將要輸出的音頻的所有信息。

在我們展示如何建立之前,讓我們先解釋一下電腦是如何處理音頻的。

數字音頻是由一長串的樣本流組成的。每個樣本表示聲音波形中的一個值。聲音按照一個特定 的採樣率來進行錄製,採樣率表示以多快的速度來播放這段樣本流,它的表示方式爲每秒多少次採樣。例如22050和44100的採樣率就是電臺和CD常用的 採樣率。此外,大多音頻有不只一個通道來表示立體聲或者環繞。例如,如果採樣是立體聲,那麼每次的採樣數就爲2個。當我們從一個電影文件中等到數據的時 候,我們不知道我們將得到多少個樣本,但是ffmpeg將不會給我們部分的樣本――這意味着它將不會把立體聲分割開來。

採樣率:簡單地說就是通過波形採樣的方法記錄1秒鐘長度的聲音,需要多少個數據。44KHz採樣率的聲音就是要花費44000個數據來描述1秒鐘的聲音波形。原則上採樣率越高,聲音的質量越好。

SDL播放聲音的方式是這樣的:你先設置聲音的選項:採樣率(在SDL的結構體中被叫做freq的表示頻率frequency),聲音通道數和其它的參 數,然後我們設置一個回調函數和一些用戶數據userdata。當開始播放音頻的時候,SDL將不斷地調用這個回調函數並且要求它來向聲音緩衝填入一個特 定的數量的字節。當我們把這些信息放到SDL_AudioSpec結構體中後,我們調用函數SDL_OpenAudio()就會打開聲音設備並且給我們送 回另外一個AudioSpec結構體。這個結構體是我們實際上用到的--因爲我們不能保證得到我們所要求的。

設置音頻

目前先把講的記住,因爲我們實際上還沒有任何關於聲音流的信息。讓我們回過頭來看一下我們的代碼,看我們是如何找到視頻流的,同樣我們也可以找到聲音流。

//窮舉流信息

videoStream=-1;

audioStream=-1;

for(i=0; i < pFormatCtx->nb_streams; i++) {

if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO
&& videoStream < 0)
{
videoStream=i;
}

if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO
&& audioStream < 0)
{
audioStream=i;
}

}

if(videoStream==-1)

return -1;

if(audioStream==-1)

return -1;

從這裏我們可以從描述流的AVCodecContext中得到我們想要的信息,就像我們得到視頻流的信息一樣。

AVCodecContext *aCodecCtx;

aCodecCtx=pFormatCtx->streams[audioStream]->codec;

包含在編解碼上下文(就是aCodecCtx)中的所有信息正是我們所需要的用來建立音頻的信息

wanted_spec.freq = aCodecCtx->sample_rate;

wanted_spec.format = AUDIO_S16SYS;

wanted_spec.channels = aCodecCtx->channels;

wanted_spec.silence = 0;

wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;

wanted_spec.callback = audio_callback;

wanted_spec.userdata = aCodecCtx;

if(SDL_OpenAudio(&wanted_spec, &spec) < 0)

{

fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;

}

讓我們瀏覽一下這些:

·freq 前面所講的採樣率

·format 告訴SDL我們將要給的格式。在“S16SYS”中的S表示有符號的signed,16表示每個樣本是16位長的,SYS表示大小頭的順序是與使用的系統相同的。這些格式是由avcodec_decode_audio2爲我們給出來的輸入音頻的格式。

·channels 聲音的通道數

·silence 這是用來表示靜音的值。因爲聲音採樣是有符號的,所以0當然就是這個值。

·samples 這是當我們想要更多聲音的時候,我們想讓SDL給出來的聲音緩衝區的尺寸。一個比較合適的值在512到8192之間;ffplay使用1024。

·callback 這個是我們的回調函數。我們後面將會詳細討論。

·userdata 這個是SDL供給回調函數運行的參數。我們將讓回調函數得到整個編解碼的上下文;你將在後面知道原因。

最後,我們使用SDL_OpenAudio函數來打開聲音。

如果你還記得前面的指導,我們仍然需要打開聲音編解碼器本身。這是很顯然的。

AVCodec *aCodec;

aCodec = avcodec_find_decoder(aCodecCtx->codec_id); //找到音頻解碼器

if(!aCodec) {

fprintf(stderr, "Unsupported codec!\n");
return -1;

}

avcodec_open(aCodecCtx, aCodec); //打開解碼器

隊列

嗯!現在我們已經準備好從流中取出聲音信息。但是我們如何來處理這些信息呢?我們將會不斷地從文件中得到這些包,但同時SDL也將調用回調函數。解決方法 爲創建一個全局的結構體變量以便於我們從文件中得到的聲音包有地方存放同時也保證SDL中的聲音回調函數audio_callback能從這個地方得到聲 音數據。所以我們要做的是創建一個包的隊列queue。在ffmpeg中有一個叫AVPacketList的結構體可以幫助我們,這個結構體實際是一串包 的鏈表。下面就是我們的隊列結構體:

typedef struct PacketQueue {

AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;

DL_mutex *mutex;

SDL_cond *cond;

} PacketQueue;

首先,我們應當指出nb_packets是與size不一樣的--size表示我們從packet->size中得到的字節數。你會注意到我們有一 個互斥量mutex和一個條件變量cond在結構體裏面。這是因爲SDL是在一個獨立的線程中來進行音頻處理的。如果我們沒有正確的鎖定這個隊列,我們有 可能把數據搞亂。我們將來看一個這個隊列是如何來運行的。每一個程序員應當知道如何來生成的一個隊列,但是我們將把這部分也來討論從而可以學習到SDL的 函數。

一開始我們先創建一個函數來初始化隊列

void packet_queue_init(PacketQueue *q)

{

memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();

}

接着我們再做一個函數來給隊列中填入東西

int packet_queue_put(PacketQueue *q, AVPacket *pkt) //將包中的數據放到隊列中去

{

AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt; //pkt放到pkt1中
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt) //pkt1加入到q
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;

}

函數SDL_LockMutex()鎖定隊列的互斥量以便於我們向隊列中添加東西,然後函數SDL_CondSignal()通過我們的條件變量爲一個接收函數(如果它在等待)發出一個信號來告訴它現在已經有數據了,接着就會解鎖互斥量並讓隊列可以自由訪問。

下面是相應的接收函數。注意函數SDL_CondWait()是如何按照我們的要求讓函數阻塞block的(例如一直等到隊列中有數據)。

int quit = 0;

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {

AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;)
{
if(quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt; //從隊列中取出數據給pkt1
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
}
else if (!block) {
ret = 0;
break;
}
else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;

}

正如你所看到的,我們已經用一個無限循環包裝了這個函數以便於我們想用阻塞的方式來得到數據。我們通過使用SDL中的函數SDL_CondWait()來 避免無限循環。基本上,所有的CondWait只等待從SDL_CondSignal()函數(或者SDL_CondBroadcast()函數)中發出 的信號,然後再繼續執行。然而,雖然看起來我們陷入了我們的互斥體中--如果我們一直保持着這個鎖,我們的函數將永遠無法把數據放入到隊列中去!但 是,SDL_CondWait()函數也爲我們做了解鎖互斥量的動作然後才嘗試着在得到信號後去重新鎖定它。

意外情況

你們將會注意到我們有一個全局變量quit,我們用它來保證還沒有設置程序退出的信號(SDL會自動處理TERM類似的信號)。否則,這個線程將不停地運行直到我們使用kill -9來結束程序。FFMPEG同樣也提供了一個函數來進行回調並檢查我們是否需要退出一些被阻塞的函數:這個函數就是url_set_interrupt_cb。

int decode_interrupt_cb(void)

{

return quit;

}

...

main() {

...

url_set_interrupt_cb(decode_interrupt_cb);

...

SDL_PollEvent(&event);

switch(event.type) {

case SDL_QUIT:

quit = 1;

...

當然,這僅僅是用來給ffmpeg中的阻塞情況使用的,而不是SDL中的。我們還必需要設置quit標誌爲1。

爲隊列提供包

剩下的我們唯一需要爲隊列所做的事就是提供包了:

PacketQueue audioq;

main() {

...

avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq);
SDL_PauseAudio(0); //開始播放音頻

函數SDL_PauseAudio()讓音頻設備最終開始工作。如果沒有立即供給足夠的數據,它會播放靜音。

我們已經建立好我們的隊列,現在我們準備爲它提供包。先看一下我們的讀取包的循環:

while(av_read_frame(pFormatCtx, &packet)>=0) { //從流中讀取包數據放到packet

if(packet.stream_index==videoStream) {
// Decode video frame
....
}
else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet); //向隊列填充東西
}
else {
av_free_packet(&packet);
}
}

注意:我們沒有在把包放到隊列裏的時候釋放它,我們將在解碼後來釋放它。

取出包

現在,讓我們最後讓聲音回調函數audio_callback來從隊列中取出包。回調函數的格式必需爲void callback(void *userdata, Uint8 *stream, int len),這裏的userdata就是我們給到SDL的指針,stream是我們要把聲音數據寫入的緩衝區指針,len是緩衝區的大小。下面就是代碼:

void audio_callback(void *userdata, Uint8 *stream, int len) {

AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
static unsigned int audio_buf_size = 0;
static unsigned int audio_buf_index = 0;
while(len > 0) {
if(audio_buf_index >= audio_buf_size) {
audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf));
if(audio_size < 0) {
audio_buf_size = 1024;
memset(audio_buf, 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_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}

}

這基本上是一個簡單的從另外一個我們將要寫的audio_decode_frame()函數中獲取數據的循環,這個循環把結果寫入到中間緩衝區,嘗試着向 流中寫入len字節並且在我們沒有足夠的數據的時候會獲取更多的數據或者當我們有多餘數據的時候保存下來爲後面使用。這個audio_buf的大小爲 1.5倍的聲音幀的大小以便於有一個比較好的緩衝,這個聲音幀的大小是ffmpeg給出的。

最後解碼音頻

讓我們看一下解碼器的真正部分:audio_decode_frame

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

static AVPacket pkt;
static uint8_t *audio_pkt_data = NULL;
static int audio_pkt_size = 0;
int len1, data_size;
for(;;) {
while(audio_pkt_size > 0) {
data_size = buf_size;
len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf, &data_size,
audio_pkt_data, audio_pkt_size);
if(len1 < 0) {
audio_pkt_size = 0;
break;
}
audio_pkt_data += len1;
audio_pkt_size -= len1;
if(data_size <= 0) {
continue;
}
return data_size;
}
if(pkt.data)
av_free_packet(&pkt);
if(quit) {
return -1;
}
if(packet_queue_get(&audioq, &pkt, 1) < 0) {
return -1;
}

audio_pkt_data = pkt.data;
audio_pkt_size = pkt.size;
}

}

整個過程實際上從函數的尾部開始,在這裏我們調用了packet_queue_get()函數。我們從隊列中取出包,並且保存它的信息。然後,一旦我們有了可以使用的包,我們就調用函數avcodec_decode_audio2(),它的功能就像它的姐妹函數 avcodec_decode_video()一樣,唯一的區別是它的一個包裏可能有不止一個聲音幀,所以你可能要調用很多次來解碼出包中所有的數據。同時也要記住進行指針audio_buf的強制轉換,因爲SDL給出的是8位整型緩衝指針而ffmpeg給出的數據是16位的整型指針。你應該也會注意到 len1和data_size的不同,len1表示解碼使用的數據的在包中的大小,data_size表示實際返回的原始聲音數據的大小。

當我們得到一些數據的時候,我們立刻返回來看一下是否仍然需要從隊列中得到更加多的數據或者我們已經完成了。如果我們仍然有更加多的數據要處理,我們把它保存到下一次。如果我們完成了一個包的處理,我們最後要釋放它。

就是這樣。我們利用主的讀取隊列循環從文件得到音頻並送到隊列中,然後被audio_callback函數從隊列中讀取並處理,最後把數據送給SDL,於是SDL就相當於我們的聲卡。讓我們繼續並且編譯:

gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \

`sdl-config --cflags --libs`

啊哈!視頻雖然還是像原來那樣快,但是聲音可以正常播放了。這是爲什麼呢?因爲聲音信息中的採樣率--雖然我們把聲音數據儘可能快的填充到聲卡緩衝中,但是聲音設備卻會按照原來指定的採樣率來進行播放。

我們幾乎已經準備好來開始同步音頻和視頻了,但是首先我們需要的是一點程序的組織。用隊列的方式來組織和播放音頻在一個獨立的線程中工作的很好:它使得程 序更加更加易於控制和模塊化。在我們開始同步音視頻之前,我們需要讓我們的代碼更加容易處理。所以下次要講的是:創建一個線程。

發佈了48 篇原創文章 · 獲贊 30 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章