指導3:播放聲音

如今咱們要來播放聲音。SDL也爲咱們準備了輸出聲音的方法。函數SDL_OpenAudio()自己就是用來打開聲音設備的。它使用一個叫作SDL_AudioSpec結構體做爲參數,這個結構體中包含了咱們將要輸出的音頻的全部信息。程序員

在咱們展現如何創建以前,讓咱們先解釋一下電腦是如何處理音頻的。數字音頻是由一長串的樣本流組成的。每一個樣本表示聲音波形中的一個值。聲音按照一個特定的採樣率來進行錄製,採樣率表示以多快的速度來播放這段樣本流,它的表示方式爲每秒多少次採樣。例如22050和44100的採樣率就是電臺和CD經常使用的採樣率。此外,大多音頻有不僅一個通道來表示立體聲或者環繞。例如,若是採樣是立體聲,那麼每次的採樣數就爲2個。當咱們從一個電影文件中等到數據的時候,咱們不知道咱們將獲得多少個樣本,可是ffmpeg將不會給咱們部分的樣本――這意味着它將不會把立體聲分割開來。ide

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

 

設置音頻函數

 

目前先把講的記住,由於咱們實際上尚未任何關於聲音流的信息。讓咱們回過頭來看一下咱們的代碼,看咱們是如何找到視頻流的,一樣咱們也能夠找到聲音流。學習

// Find the first video streamui

videoStream=-1;this

audioStream=-1;url

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

  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; // Didn't find a video stream

if(audioStream==-1)

  return -1;

從這裏咱們能夠從描述流的AVCodecContext中獲得咱們想要的信息,就像咱們獲得視頻流的信息同樣。

AVCodecContext *aCodecCtx;

 

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

包含在編解碼上下文中的全部信息正是咱們所須要的用來創建音頻的信息:

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;

  SDL_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;

  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()鎖定隊列的互斥量以便於咱們向隊列中添加東西,而後函數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;

    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) {

  // Is this a packet from the video stream?

  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`

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

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

相關文章
相關標籤/搜索