指 导 3:播放声音
音频
现在我们要来播放声音。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;
包含在编解码上下文中的所有信息正是我们所需要的用来建立音频的信息:
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_audio4为我们给出来的输入音频的格式。
·channels声音的通道数
·silence 这是用来表示静音的值。因为声音采样是有符号的,所以 0 当然就是这个值。
·samples这是当我们想要更多声音的时,SDL 所需的声音缓冲大小。一个比较合适的值
在 512 到 8192 之间;ffplay 使用 1024。
·callback这个是我们的回调函数。我们后面将会详细讨论。
·userdata这个是 SDL 供给回调函数运行的参数。我们将让回调函数得到整个编解码的上下文;
你将在后面知道原因。
最后,我们使用 SDL_OpenAudio 函数来打开声音。 如果你还记得前面的指导,我们仍然需要
打开声音编解码器本身。这是很显然的。
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 中得到的字节数。你会注意到 在结构体里有一 个互斥量 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模式,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 ret; }
正如你所看到的,我们已经用一个无限循环包装了这个函数以便于用阻塞 的方式来得到
数据。我们通过使用 SDL 中的函数 SDL_CondWait()来避免无限循 环。基本上,所有的 CondWait
只是等待从 SDL_CondSignal()函数(或者 SDL_CondBroadcast()函数)中发出的信号,然后再
继续执行。然而,虽然看起 来我们陷入了我们的互斥体中--如果我们一直保持着这个锁,
我们的函数将永远无法把数据放入到队列中去!但是,SDL_CondWait()函数会为我们解锁的
传入 互斥量,然后才尝试着在得到信号后去重新锁定它。
意外情况
你们将会注意到我们有一个全局变量 quit,我们用它来保证还没有设置退 出信号(SDL
会自动处理 TERM 类似的信 号)。否则,这个线程将不停地运行,直到我们使用 kil l-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[(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 字节并且在我们没有足够的数据的时
候会尝试获取更多的数据或者当我们有多余数 据的时候保存下来为后面使用。ffmpeg 给出的
audio_buf 的大小为 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; 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; } }
整个过程实际上从函数的尾部开始,在这里我们调用了 packet_queue_get()函 数。我们从队列中
取出包,并且保存它的信息。然后,一旦我们有了可以使用 的包,我们就调用函数
avcodec_decode_audio4(),它的功能就像它的姐妹函数 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`
啊哈!视频虽然还是像原来那样快,但是声音可以正常播放了。这是为什么呢? 因为声音信息中的
采样率--虽然我们把声音数据尽可能快的填充到声卡缓冲 中,但是声音设备却会按照原来指定的采样率
来进行播放。
我们几乎已经准备好来开始同步音频和视频了,但是首先我们需要的是一点程序的组织。用队列的
方式来组织和播放音频在一个独立的线程中工作的很好:它使 得程序更加更加易于控制和模块化。在我们
开始同步音视频之前,我们需要让 我们的代码更加容易处理。所以下次要讲的是:创建一个线程。