ffmpeg的tutorial中文版學習筆記(三)

fmpeg 教程3:播放聲音
源代碼:tutorial03-1.c
          現在我們要來播放聲音。SDL 也爲我們準備了輸出聲音的方法。函數SDL_OpenAudio()本身就是用來打開聲音設備的。它使用一個叫做SDL_AudioSpec 結構體作爲參數,這個結構體中包含了我們將要輸出的音頻的所有信息。
          在我們展示如何建立之前,讓我們先解釋一下電腦是如何處理音頻的。數字音頻是由一長串的樣本流組成的。每個樣本表示聲音波形中的一個值。聲音按照一個特定的採樣率來進行錄製,採樣率表示以多快的速度來播放這段樣本流,它的表示方式爲每秒多少次採樣。例如22050和44100的採樣率就是電臺和CD 常用的採樣率。此外,大多音頻有不只一個通道來
表示立體聲或者環繞。例如,如果採樣是立體聲,那麼每次的採樣數就爲2個。當我們從一個電影文件中等到數據的時候,我們不知道我們將得到多少個樣本,但是ffmpeg 將不會給我們部分的樣本 ――這意味着它將不會把立體聲分割開來。
          SDL 播放聲音的方式是這樣的:你先設置聲音的選項:採樣率(在SDL的結構體中被叫做freq 的表示頻率frequency),聲音通道數和其它的參數,然後我們設置一個回調函數和一些用戶數據userdata。當開始播放音頻的時候,SDL 將不斷地調用這個回調函數並且要求它來向聲音緩衝填入一個特定的數量的字節。當我們把這些信息放到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->streams[i]->codec->codec_type==AVMEDIA_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 = NULL;
  aCodecCtx=pFormatCtx->streams[audioStream]->codec;
包含在編解碼上下文中的所有信息正是我們所需要的用來建立音頻的信息:
	SDL_AudioSpec wanted_spec, spec;
	// Set audio settings from codec info
	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 = NULL;
	AVDictionary *audioOptionsDict = NULL;
	aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
	if(!aCodec)
	{
		fprintf(stderr, "Unsupported codec!\n");
		return -1;
	}
	avcodec_open2(aCodecCtx, aCodec, &audioOptionsDict);
隊列
          嗯!現在我們已經準備好從流中取出聲音信息。但是我們如何來處理這些信息呢?我們將會不斷地從文件中得到這些包,但同時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 中得到的字節數(所有packets包的)。你會注意到我們有一個互斥量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爲空,則q->first_pkt=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;
}
函數av_dup_packet()原形:int av_dup_packet(AVPacket *pkt) ;
 AVPacket  的data 在內存中buffer有兩種情況:
1)由av_malloc申請的獨立的buffer(unshared buffer);
2)是其他AVPacket或者其他reuseable 內存的一部分(shared buffer); av_dup_packet作用是通過調用 av_malloc、memcpy、memset等函數, 將shared buffer 的AVPacket duplicate(複製)到獨立的buffer中。並且修改AVPacket的析構函數指針av_destruct_pkt。
          函數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_CondWait(SDL_cond*  cond,SDL_mutex* mutex)
int SDL_CondWait(SDL_cond*  cond,SDL_mutex* mutex)
cond :the condition variable to wait on
mutex:the mutex used to coordinate thread access
Returns 0 when it is signaled or a negative error code on failure; call SDL_GetError() for more information.
This function unlocks the specified mutex and waits for another thread to call SDL_CondSignal() or SDL_CondBroadcast() on the condition variable cond. Once the condition variable is signaled, the mutex is re-locked and the function returns.
The mutex must be locked before calling this function.
This function is the equivalent of calling SDL_CondWaitTimeout() with a time length of SDL_MUTEX_MAXWAIT. 
          正如你所看到的,我們已經用一個無限循環包裝了這個函數以便於我們想用阻塞的方式來得到數據。我們通過使用SDL 中的函數SDL_CondWait()來避免無限循環。基本上,所有的CondWait 只等待從SDL_CondSignal()函數(或者SDL_CondBroadcast()函數)中發出的信號,然後再繼續執行。然而,雖然看起來我們陷入了我們的互斥體中--如果我們一直保持着這個鎖,我們的函數將永遠無法把數據放入到隊列中去!但是,SDL_CondWait()函數也爲我們做了解鎖互斥量的動作然後才嘗試着在得到信號後去重新鎖定它
意外情況
          你們將會注意到我們有一個全局變量quit,我們用它來保證還沒有設置程序退出的信號(SDL 會自動處理TERM 類似的信號)。否則,這個線程將不停地運行直到我們使用kill -9來結束程序,必需要設置quit 標誌爲1。
main(){
	...
	SDL_PollEvent(&event);
	switch(event.type){
		case SDL_QUIT:
     		quit = 1;
     	...
爲隊列提供包
剩下的我們唯一需要爲隊列所做的事就是提供包了:
PacketQueue audioq;
int main(int argc, char *argv[])
{
	 ......
	avcodec_open2(aCodecCtx, aCodec, &audioOptionsDict);

	// audio_st = pFormatCtx->streams[index]
	packet_queue_init(&audioq);
	SDL_PauseAudio(0);
	//SDL_PauseAudio庫函數可以暫停或者恢復audio_callback函數的執行,0是恢復,其他的是暫停
函數SDL_PauseAudio()讓音頻設備最終開始工作。如果沒有立即供給足夠的數據,它會播放靜音。
我們已經建立好我們的隊列,現在我們準備爲它提供包。先看一下我們的讀取包的循環:

// Read frames and save first five frames to disk
	i=0;
	while(av_read_frame(pFormatCtx, &packet)>=0)
	{
		// Is this a packet from the video stream?
		if(packet.stream_index==videoStream)
		{
			// Decode video frame
			avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,&packet);

			// Did we get a video frame?
			if(frameFinished)
			{
				SDL_LockYUVOverlay(bmp);

				AVPicture pict;
				pict.data[0] = bmp->pixels[0];
				pict.data[1] = bmp->pixels[2];
				pict.data[2] = bmp->pixels[1];

				pict.linesize[0] = bmp->pitches[0];
				pict.linesize[1] = bmp->pitches[2];
				pict.linesize[2] = bmp->pitches[1];

				// Convert the image into YUV format that SDL uses
				sws_scale(sws_ctx,(uint8_t const * const *)pFrame->data,pFrame->linesize,0,
                                           pCodecCtx->height,pict.data,pict.linesize);

				SDL_UnlockYUVOverlay(bmp);

				rect.x = 0;
				rect.y = 0;
				rect.w = pCodecCtx->width;
				rect.h = pCodecCtx->height;
				SDL_DisplayYUVOverlay(bmp, &rect);
				av_free_packet(&packet);
			}
		}
		else if(packet.stream_index==audioStream)
		{
			packet_queue_put(&audioq, &packet);
		}
		else
		{
			av_free_packet(&packet);
		}
		// Free the packet that was allocated by av_read_frame
		SDL_PollEvent(&event);
		switch(event.type)
		{
			case SDL_QUIT:
				quit = 1;
				SDL_Quit();
				exit(0);
				break;
			default:
				break;
		}

	}
注意:我們沒有在把包放到隊列裏的時候釋放它,我們將在解碼後來釋放它。
取出包
          現在,讓我們最後讓聲音回調函數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[(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, audio_buf_size);
            if(audio_size<0)
            {
                /* If error, output silence */
                audio_buf_size = 1024; // arbitrary?
                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 字節並且在我們沒有足夠的數據的時候會獲取更多的數據或者當我們有多餘數據的時候保存下來爲後面使用(利用static類型實現)。這個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;
    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;
            if (got_frame)
            {
                data_size =
                av_samples_get_buffer_size
                (
                NULL,
                aCodecCtx->channels,
                frame.nb_samples,
                aCodecCtx->sample_fmt,
                1
                );
                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;
    }
}
關於:int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame, int *got_frame_ptr, const AVPacket *avpkt):
/**
* Decode the audio frame of size avpkt->size from avpkt->data into frame.
*
* Some decoders may support multiple frames in a single AVPacket. Such
* decoders would then just decode the first frame and the return value would be
* less than the packet size. In this case, avcodec_decode_audio4 has to be
* called again with an AVPacket containing the remaining data in order to
* decode the second frame, etc...  Even if no frames are returned, the packet
* needs to be fed to the decoder with remaining data until it is completely
* consumed or an error occurs.
*
* Some decoders (those marked with CODEC_CAP_DELAY) have a delay between input
* and output. This means that for some packets they will not immediately
* produce decoded output and need to be flushed at the end of decoding to get
* all the decoded data. Flushing is done by calling this function with packets
* with avpkt->data set to NULL and avpkt->size set to 0 until it stops
* returning samples. It is safe to flush even those decoders that are not
* marked with CODEC_CAP_DELAY, then no samples will be returned.
*
* @warning The input buffer, avpkt->data must be FF_INPUT_BUFFER_PADDING_SIZE
*          larger than the actual read bytes because some optimized bitstream
*          readers read 32 or 64 bits at once and could read over the end.
*
* @param      avctx the codec context
* @param[out] frame The AVFrame in which to store decoded audio samples.
*                   The decoder will allocate a buffer for the decoded frame by
*                   calling the AVCodecContext.get_buffer2() callback.
*                   When AVCodecContext.refcounted_frames is set to 1, the frame is
*                   reference counted and the returned reference belongs to the
*                   caller. The caller must release the frame using av_frame_unref()
*                   when the frame is no longer needed. The caller may safely write
*                   to the frame if av_frame_is_writable() returns 1.
*                   When AVCodecContext.refcounted_frames is set to 0, the returned
*                   reference belongs to the decoder and is valid only until the
*                   next call to this function or until closing or flushing the
*                   decoder. The caller may not write to it.
* @param[out] got_frame_ptr Zero if no frame could be decoded, otherwise it is
*                           non-zero. Note that this field being set to zero
*                           does not mean that an error has occurred. For
*                           decoders with CODEC_CAP_DELAY set, no given decode
*                           call is guaranteed to produce a frame.
* @param[in]  avpkt The input AVPacket containing the input buffer.
*                   At least avpkt->data and avpkt->size should be set. Some
*                   decoders might also require additional fields to be set.
* @return A negative error code is returned if an error occurred during
*         decoding, otherwise the number of bytes consumed from the input
*         AVPacket is returned.
*/
關於:int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples, enum AVSampleFormat sample_fmt, int align):
/**
* Get the required buffer size for the given audio parameters.
*
* @param[out] linesize calculated linesize, may be NULL
* @param nb_channels   the number of channels
* @param nb_samples    the number of samples in a single channel
* @param sample_fmt    the sample format
* @param align         buffer size alignment (0 = default, 1 = no alignment)
* @return              required buffer size, or negative error code on failure
*/
          整個過程實際上從函數的尾部開始,在這裏我們調用了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 ./tutorial03-1.c -o ./tutorial03-1 -lavutil -lavformat -lavcodec -lswscale  -lz -lm `sdl-config --cflags --libs` -I /home/Jiakun/ffmpeg_build/include/ -L /home/Jiakun/ffmpeg_build/lib/ -I /usr/include/SDL/

          啊哈!視頻雖然還是像原來那樣快,但是聲音可以正常播放了。這是爲什麼呢?因爲聲音信息中的採樣率--雖然我們把聲音數據儘可能快的填充到聲卡緩衝中,但是聲音設備卻會按照原來指定的採樣率來進行播放。我們幾乎已經準備好來開始同步音頻和視頻了,但是首先我們需要的是一點程序的組織。用隊列的方式來組織和播放音頻在一個獨立的線程中工作的很好:它使得程序更加更加易於控制和模塊化。在我們開始同步音視頻之前,我們需要讓我們的代碼更加容易處理。所以下次要講的是:創建一個線程。
源代碼: 見這裏的github;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章