ffmpeg 和 SDL的教程 tutorial3學習--播放聲音


tutorial3學習

現在我們要播放音頻。SDL也停工了輸出聲音的方法。SDL_OpenAudio 函數就是用來打開音頻設備的。他有一個結構體位SDL_AudioSpec的參數 ,這個參數包含了我們要輸出的音頻的所有信息。
在告訴你如何設置之前,我們先解釋一下音頻是如何被計算機處理的。數字音頻包含了採樣的流,每個採樣代表了一個音頻波的值。聲音以某種採樣率來錄製,這說明了播放每個採樣需要多快的速度,以每秒播放多少個採樣來計算。例如常見的採樣率是22050 和 44100,分別用於radio 和CD。此外,大多數音頻有幾個channel,這樣可以有立體聲或者環繞音,例如,如果採樣時立體聲,採樣數據應該是每次得到兩個。當我們從電影文件中得到數據時,我們不知道要得到多少採樣,但是ffmpeg不會給我們部分採樣---這也意味着它不會把立體聲採樣分開。

SDL播放音頻的方法如下:設置音頻選項:採樣率(叫做freq),通道數量,等等。同時設置一個 回調函數和userdata。當我們開始播放音頻時,SDL將持續的調用回調函數,且用一些字節來填充聲音buffer。把這些信息放到SDL_AudioSpec結構體後,調用SDL_OpenAudio(), 這將打開音頻設備,給我們另外一個AudioSpec結構體。這是我們使用的步驟---我麼不能保證得到了所需的。

#####設置音頻
請先記住這些概念,因爲我們到目前爲止還沒有音頻流的任何信息。讓我們回到代碼,我們將找到視頻流和音頻流。

//Find the first video stream
videoStream = -1;
audioStream = -1
for(i=0; i<pFormatCtx->nb_streams; i++)
{
    if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && videoStream<0)
{
    videoStream = i;
}
if(pFormatCtx->stream[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO &&audioStream<0)
{
    audioStream = i;
}
}
if(videoStream == -1)
return -1;
if(audioStream == -1)
return -1;
從這裏我們能夠從流中通過AVCodecContex結構體得到所有的信息,就像之前處理視頻時候。
AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;
aCodecCtxOrig = pFormatCtx->streams[audioStream]->codec;
如果你還記得上個例子,我們將要自己 打開audio codec。下面是直截了當的:
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtxOrig->codec_id);
if(!aCodec)
{
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
//Copy context
aCodecCtx = avcodec_alloc_context3(aCodec);
if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0)
{
fprintf(stderr, "Couldn't copy codec context");
return -1;
}
/* set up SDL Audio here*/
avcodec_open2(aCodecCtx, aCodec, NULL);
context中包含的信息就是我們設置音頻是需要的信息。

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)<0(
{
    fprintf(stderr, "SDL_OpenAudio:%s\n", SDL_GetError());
}
我們來解釋一下:
freq: 採樣率
format:這告訴SDL我們將使用什麼格式。S16SYS中的第一個S表示“signed"(有符號),16表示每個採樣有16位,SYS表示大小端順序依賴系統。這就是avcodec_decode_audio2告訴我們進來的音頻的格式。
channels: 音頻通道的數量
silence:這是表示是否靜音。因爲音頻是signed,0就是默認值。
samples:這個表示當SDL需要更多音頻時,我們要SDL分配的buffer的大小。一個比較好的值是512和8192之間的值。ffplay用1024.
callback這裏我們傳遞真正的回調函數。下面再詳細討論。
userdata:SDL給我們的回調函數一個紙箱userdata的void指針,這個數據時我們的回調函數需要的。我們要讓它知道我們的codec context;
最後我們用 SDL_OpenAudio 打開音頻。

隊列:
這裏我們準好了從stream中拉取音頻信息。但是我們用這些信息做什麼呢?我們將持續不斷的從movie文件中獲取packets,同時SDL將調用回調函數。這種解決方案將創建一些用來存放audio packets的全局結構體,所以 audio_callback需要從audio data中獲取某些信息。所以我們要做的就是create一個 packets的隊列。ffmpeg提供了這樣一個結構體來幫助我們:AVPacketList,這是一個packets的鏈表。下面是隊列的結構體。
typedef struct PacketQueue
{
    AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutext *mutex;
SDL_cond *cond;
}PacketQueue;

首先,需要指出 nb_packets 和 size 不同。size表示我們從packet_size中得到的字節大小。你會注意到我們有一個mutex 和 condition變量。這是因爲SDL在一個單獨的線程裏面處理音頻。如果不正確的lock住隊列,我們會弄混數據。我們在處理隊列的時候會看到。每個人都應該知道怎樣make a queue,但是我們仍舊寫下來讓大家學習SDL函數。
首先,寫一個函數初始化queue:
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;
pkt1->next = NULL;

SDL_LockMutex(q->mutex);
if(!q->last_pkt)
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 鎖住了隊列中的mutex,這樣我們就能往隊列中添加東西了。SDL_CondSignal()給函數發送了一個信號,告訴它有了數據,可以處理,然後unlock mutex,繼續。
下面是相應的get 函數,注意SDL_CondWait,使得函數阻塞(例如在這裏暫停直到獲得數據)。
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;
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 get;
}
就像你看到的,我們在函數裏有個for無限循環,所以能夠如果想阻塞的話確保得到數據。我們使用SDL_CondWait來避免死循環。基本來說,CondWait做的就是等待SDL_CondSignal()發送一個信號(或者SDL_CondBroadcast),然後繼續。然而,看起來像是在mutex中獲取--如果我們拿到lock,我們的put函數不能往隊列中put任何數據。然而,SDL_CondWait做的就是unlock這個mutex,然後試圖鎖住它,當我們再次得到信號的時候。

你會注意到我們有一個quit變量,我們用來沒有給程序設置一個退出信號。SDL自動的處理了TERM信號。否則,線程將會無限循環下去。
SDL_PollEvent(&event);
switch(event.type)
{
case SDL_QUIT:
quit = 1;
}
我們保證把quit置爲1;

填充packet
剩下的事情就是建立我們的queue

PacketQueue audioq;

main()
{
......
avcodec_open2(aCodecCtx, aCodec, NULL);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
}

SDL_PauseAudio 啓動音頻設備。如果沒有得到數據,就播放靜音。它不會馬上。

到這裏,我們建立了queue,現在我們開始填充packet。我們到packet的讀循環去:

while(av_read_frame(pFormatCtx, &packet) >=0)
{
//Is this  a packet from the video stream?
if(packet.stream_index == videoStream)
{
//Decode video frame
}
if(packet.stream_index == audioStream)
{
packet_queue_put(&audioq, &packet);
}
else
{
av_free_packet(&packet);
}
}
注意到我們把packet放到隊列中後沒有馬上free,我們在decode之後纔去free。

### 得到packet
讓我們的 audio_callback函數來獲取queue上的packets。回調函數必須格式爲 
void callback(void *userdata, Uint8 *stream, int len)
userdata 是我們給SDL 的指針,stream是我們把audio data 寫入的指針,len是buffer的長度。下面是代碼

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)
{
//We have already sent all our data; get more
audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf));
if(audio_size < 0)
{
//if error, output silence
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>1en)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream ==len1;
audio_buf_index +=len1;
}
}
這是一個簡單的循環,從另外一個function中獲取data, audio_decode_frame, 把結果存儲在一個臨時buffer中,試圖 寫len長度個字節到stream中,並且當沒有足夠數據時get more data。或者當有多餘的data時把它保存起來。audio_buf的大小是audio_frame的1.5倍,這給了我們一個好的緩衝。

### 最後解碼音頻
讓我們看看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;
      static AVFrame frame;

      int len1, data_size = 0;

      for(;;) {
        while(audio_pkt_size > 0) {
          int got_frame = 0;
          len1 = avcodec_decode_audio4(aCodecCtx, &frame;, &got;_frame, &pkt;);
          if(len1 < 0) {
        /* if error, skip frame */
        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(NULL, 
                               aCodecCtx->channels,
                               frame.nb_samples,
                               aCodecCtx->sample_fmt,
                               1);
        assert(data_size <= buf_size);
        memcpy(audio_buf, frame.data[0], data_size);
          }
          if(data_size <= 0) {
        /* No data yet, get more frames */
        continue;
          }
          /* We have data, return it and come back for more later */
          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 函數。我們從queue中獲取數據,保存信息。然後,一旦有了可以處理的packet,就調用avcodec_decode_audio4(),這個和 avcodec_decode_video()功能類似,區別在於,packet可能有多幀,這樣,你必須多次調用以得到所有的數據。一旦有了frame,我們簡單的把它copy到audio buffer,保證 data_size 比 audio buffer小。並且記住 audio_buf的大小,因爲SDL處理8bit,ffmpeg處理16bit的int。也應該注意len1和data_size的區別,len1是我們使用了的packet,data_size是返回的raw data的數量。

當我們有了一些data後,我們立刻返回去看是否仍舊需要得到跟多的data,或者已經結束。如果有更多的packet要處理,我們先保存下來。如果處理完一個packet,我們把它free。

這就是整個過程。我們從主循環中得到audio然後放到queue中,然後audio_callback函數從這個隊列中讀,這個回調函數交給sdl來處理,SDL把聲音送給聲卡。

現在好了,視頻還是很快播放,但是音頻正常。爲什麼

我們需要重新組織一下代碼,這種從queue裏面獲取audio並且在單獨的線程裏面播放工作很好:這使得代碼更好管理和更模塊化。在同步音頻和視頻之前,我們要使的代碼更容易處理。
下次:產生線程。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章