FFPLAY的原理(三)

播放声音
现在我们要来播放声音。SDL也为我们准备了输出声音的方法。函数SDL_OpenAudio()本身就是用来打开声音设备的。它使用一个叫做SDL_AudioSpec结构体作为参数,这个结构体中包含了我们将要输出的音频的所有信息。
在我们展示如何建立之前,让我们先解释一下电脑是如何处理音频的。数字音频是由一长串的样本流组成的。每个样本表示声音波形中的一个值。声音按照一个特定 的采样率来进行录制,采样率表示以多快的速度来播放这段样本流,它的表示方式为每秒多少次采样。例如22050和44100的采样率就是电台和CD常用的 采样率。此外,大多音频有不只一个通道来表示立体声或者环绕。例如,如果采样是立体声,那么每次的采样数就为2个。当我们从一个电影文件中等到数据的时 候,我们不知道我们将得到多少个样本,但是ffmpeg将不会给我们部分的样本――这意味着它将不会把立体声分割开来。
SDL播放声音的方式是这样的:你先设置声音的选项:采样率(在SDL的结构体中被叫做freq的表示频率frequency),声音通道数和其它的参 数,然后我们设置一个回调函数和一些用户数据userdata。当开始播放音频的时候,SDL将不断地调用这个回调函数并且要求它来向声音缓冲填入一个特 定的数量的字节。当我们把这些信息放到SDL_AudioSpec结构体中后,我们调用函数SDL_OpenAudio()就会打开声音设备并且给我们送 回另外一个AudioSpec结构体。这个结构体是我们实际上用到的--因为我们不能保证得到我们所要求的。
设置音频
目前先把讲的记住,因为我们实际上还没有任何关于声音流的信息。让我们回过头来看一下我们的代码,看我们是如何找到视频流的,同样我们也可以找到声音流。

 1 // Find the first video stream 
 2 videoStream=-1; 
 3 audioStream=-1; 
 4 for(i=0; i < pFormatCtx->nb_streams; i++) { 
 5 if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO 
 6 && 
 7 videoStream < 0) { 
 8 videoStream=i; 
 9 } 
10 if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO && 
11 audioStream < 0) { 
12 audioStream=i; 
13 } 
14 } 
15 if(videoStream==-1) 
16 return -1; // Didn't find a video stream 
17 if(audioStream==-1) 
18 return -1; 

从这里我们可以从描述流的AVCodecContext中得到我们想要的信息,就像我们得到视频流的信息一样。

1 AVCodecContext *aCodecCtx; 
2 aCodecCtx=pFormatCtx->streams[audioStream]->codec; 

包含在编解码上下文中的所有信息正是我们所需要的用来建立音频的信息:

 1 wanted_spec.freq = aCodecCtx->sample_rate; 
 2 wanted_spec.format = AUDIO_S16SYS; 
 3 wanted_spec.channels = aCodecCtx->channels; 
 4 wanted_spec.silence = 0; 
 5 wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; 
 6 wanted_spec.callback = audio_callback; 
 7 wanted_spec.userdata = aCodecCtx; 
 8 if(SDL_OpenAudio(&wanted_spec, &spec) < 0) { 
 9 fprintf(stderr, "SDL_OpenAudio: %s
", SDL_GetError()); 
10 return -1; 
11 } 

让我们浏览一下这些:
·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函数来打开声音。
如果你还记得前面的指导,我们仍然需要打开声音编解码器本身。这是很显然的。

1 AVCodec *aCodec; 
2 aCodec = avcodec_find_decoder(aCodecCtx->codec_id); 
3 if(!aCodec) { 
4 fprintf(stderr, "Unsupported codec!
"); 
5 return -1; 
6 } 
7 avcodec_open(aCodecCtx, aCodec);

队列
嗯!现在我们已经准备好从流中取出声音信息。但是我们如何来处理这些信息呢?我们将会不断地从文件中得到这些包,但同时SDL也将调用回调函数。解决方法 为创建一个全局的结构体变量以便于我们从文件中得到的声音包有地方存放同时也保证SDL中的声音回调函数audio_callback能从这个地方得到声 音数据。所以我们要做的是创建一个包的队列queue。在ffmpeg中有一个叫AVPacketList的结构体可以帮助我们,这个结构体实际是一串包 的链表。下面就是我们的队列结构体:

1 typedef struct PacketQueue { 
2 AVPacketList *first_pkt, *last_pkt; 
3 int nb_packets; 
4 int size; 
5 SDL_mutex *mutex; 
6 SDL_cond *cond; 
7 } PacketQueue; 

首先,我们应当指出nb_packets是与size不一样的--size表示我们从packet->size中得到的字节数。你会注意到我们有一 个互斥量mutex和一个条件变量cond在结构体里面。这是因为SDL是在一个独立的线程中来进行音频处理的。如果我们没有正确的锁定这个队列,我们有 可能把数据搞乱。我们将来看一个这个队列是如何来运行的。每一个程序员应当知道如何来生成的一个队列,但是我们将把这部分也来讨论从而可以学习到SDL的 函数。
一开始我们先创建一个函数来初始化队列:

1 void packet_queue_init(PacketQueue *q) { 
2 memset(q, 0, sizeof(PacketQueue)); 
3 q->mutex = SDL_CreateMutex(); 
4 q->cond = SDL_CreateCond(); 
5 } 

接着我们再做一个函数来给队列中填入东西:

 1 int packet_queue_put(PacketQueue *q, AVPacket *pkt) { 
 2 AVPacketList *pkt1; 
 3 if(av_dup_packet(pkt) < 0) { 
 4 return -1; 
 5 } 
 6 pkt1 = av_malloc(sizeof(AVPacketList)); 
 7 if (!pkt1) 
 8 return -1; 
 9 pkt1->pkt = *pkt; 
10 pkt1->next = NULL; 
11 SDL_LockMutex(q->mutex); 
12 if (!q->last_pkt) 
13 q->first_pkt = pkt1; 
14 else 
15 q->last_pkt->next = pkt1; 
16 q->last_pkt = pkt1; 
17 q->nb_packets++; 
18 q->size += pkt1->pkt.size; 
19 SDL_CondSignal(q->cond); 
20 SDL_UnlockMutex(q->mutex); 
21 return 0; 
22 } 

函数SDL_LockMutex()锁定队列的互斥量以便于我们向队列中添加东西,然后函数SDL_CondSignal()通过我们的条件变量为一个接收函数(如果它在等待)发出一个信号来告诉它现在已经有数据了,接着就会解锁互斥量并让队列可以自由访问。
下面是相应的接收函数。注意函数SDL_CondWait()是如何按照我们的要求让函数阻塞block的(例如一直等到队列中有数据)。

 1 int quit = 0; 
 2 static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) { 
 3 AVPacketList *pkt1; 
 4 int ret; 
 5 SDL_LockMutex(q->mutex); 
 6 for(;;) { 
 7 if(quit) { 
 8 ret = -1; 
 9 break; 
10 } 
11 pkt1 = q->first_pkt; 
12 if (pkt1) { 
13 q->first_pkt = pkt1->next; 
14 if (!q->first_pkt) 
15 q->last_pkt = NULL; 
16 q->nb_packets--; 
17 q->size -= pkt1->pkt.size; 
18 *pkt = pkt1->pkt; 
19 av_free(pkt1); 
20 ret = 1; 
21 break; 
22 } else if (!block) { 
23 ret = 0; 
24 break; 
25 } else { 
26 SDL_CondWait(q->cond, q->mutex); 
27 } 
28 } 
29 SDL_UnlockMutex(q->mutex); 
30 return ret; 
31 } 

正如你所看到的,我们已经用一个无限循环包装了这个函数以便于我们想用阻塞的方式来得到数据。我们通过使用SDL中的函数 SDL_CondWait()来避免无限循环。基本上,所有的CondWait只等待从SDL_CondSignal()函数(或者 SDL_CondBroadcast()函数)中发出的信号,然后再继续执行。然而,虽然看起来我们陷入了我们的互斥体中--如果我们一直保持着这个锁, 我们的函数将永远无法把数据放入到队列中去!但是,SDL_CondWait()函数也为我们做了解锁互斥量的动作然后才尝试着在得到信号后去重新锁定 它。

原文地址:https://www.cnblogs.com/djzny/p/3399520.html