FFmpeg和SDL教程之三(Playing Sound)



Audio


现在我们开始加入声音,SDL也提供了输出声音的方法。SDL_OpenAudio() 函数可以自己打开音频设备。它需要一个 SDL_AudioSpec 结构体作为参数,该结构体包含了所有我们将要输出音频的信息。


先讲一下音频是如何在计算机中处理的。数字音频包含着很长的采样流(long stream of samples)。每个采样代表着一个音波的值。声音被某些特定的采样率记录下来。这表示以多快的速度播放这些采样,每一秒会有很多采样。例如采样率有:22050、44100个样本每秒,这分别是用作广播和CD的速率。此外,大部分立体或者环绕音频都有不止一个通道。例如,如果是立体声采样,每次会有两个样本。当我们从电影文件中获取数据,没法知道会得到多少采样(samples),但是 ffmpeg 会提供部分采样,这意味着不会将一个 stereo sample 分开。


SDL 播放音频的方法是:你设置音频选项 -- the sample rate(called "freq" for frequency in the SDL struct),number of channels,wo also set a callback function and userdata。当我们开始播放音频,SDL 将会持续的调用这个回调函数并且向 音频缓冲区填充一些字节。在我们将信息填入 SDL_AudioSpec 结构体后,我们调用 SDL_OpenAudio()函数,它会打开音频设备并且向我们返回另一个 AudioSpec 结构体。



Setting Up the Audio


让我们回到找到视频流的代码出,找到哪个流是音频流:

// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
  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;

Contained within this codec context is all the information we need to set up our audion:

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: The sample rate, as explained earlier.
  • format: This tells SDL what format we will be giving it. The "S" in "S16SYS" stands for "signed", the 16 says that each sample is 16 bits long, and "SYS" means that the endian-order will depend on the system you are on. This is the format that avcodec_decode_audio2 will give us the audio in.
  • channels: Number of audio channels.
  • silence: This is the value that indicated silence. Since the audio is signed, 0 is of course the usual value.
  • samples: This is the size of the audio buffer that we would like SDL to give us when it asks for more audio. A good value here is between 512 and 8192; ffplay uses 1024.
  • callback: Here's where we pass the actual callback function. We'll talk more about the callback function later.
  • userdata: SDL will give our callback a void pointer to any user data that we want our callback function to have. We want to let it know about our codec context; you'll see why.


     最后,使用 SDL_OpenAudio 来打开音频。

    我们仍然需要打开 音频编解码器,这是很简单的:

    AVCodec         *aCodec;
    
    aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
    if(!aCodec) {
      fprintf(stderr, "Unsupported codec!\n");
      return -1;
    }
    avcodec_open(aCodecCtx, aCodec);



    Queues


    下面开始从流中得到音频信息。但是怎样处理这些信息?我们会持续的从电影文件中获得包数据,但是,SDL同时也会调用回调函数。这种方法会创建某些全局的结构体,用来缓存从 audio_callback 获得的音频包。接下来创建一些包队列,ffmpeg 中的 AVPacketList 会帮助我们实现它,AVPacketList 只是包的链表而已。下面就是 队列结构:

    <span style="font-size:14px;">typedef struct PacketQueue {
      AVPacketList *first_pkt, *last_pkt;
      int nb_packets;
      int size;
      SDL_mutex *mutex;
      SDL_cond *cond;
    } PacketQueue;</span>

     首先,we should point out that nb_packets is not the same as size -- size refers to a byte size that we get from packet->size。你会注意到有一个 mutex 和 条件变量在这里。这是因为 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() 锁住队列中的 mutex,因此我们可以向它添加一些信息,然后 SDL_CondSignal()函数发送一个信号给 get function(if it is waiting),通过我们的条件变量来告诉它有一个数据来到,可以处理啦!然后就可以解锁 mutex。

    下面介绍 get function。Notice how SDL_CondWait() makes the function block(i.e pause until we get data)if we tell it to:

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

    这里可以看到,我们将函数封装在一个 for 循环中,在我们想停止时可以得到一些数据。可以使用SDL的 SDL_CondWait()函数来避免死循环。基本上来说,所有 CondWait 做的就是 等待来自 SDL_CondSignal()的(或者SDL_CondBroadcast())信号。


     However, it looks as though we've trapped it within our mutex — if we hold the lock, our put function can't put anything in the queue! freq: The sample rate, as explained earlier.
     However, what SDL_CondWait() also does for us is to unlock the mutex we give it and then attempt to lock it again once we get the signal.



    In Case of Fire


    你同时会注意到有一个全局的退出变量可以用来确保我们没有将程序设置为退出信号。然而,线程会一直进行,不得不将其kill。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;
    ...

    This only applies for ffmpeg functions that block, of course, not SDL ones. We make sure to set thequit flag to 1.



    Feeding Packets


    设置队列:

    <span style="font-size:14px;">PacketQueue audioq;
    main() {
    ...
      avcodec_open(aCodecCtx, aCodec);
    
      packet_queue_init(&audioq);
      SDL_PauseAudio(0);</span>


    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);
      }

    注意这里并没有释放 packet 在我们将它放入队列以后。我么将会在解码完以后再释放。




    Fetching Packets


    现在调用 audio_callback 函数来取得队列中的包数据。回调函数类似于这种形式:void callback(void *userdata, Uint8 *stream, int len), userdata就是我们传递给SDL的指针,stream是我们将要写入音频流的地方,len是缓冲区的大小.示例代码如下:

    <span style="font-size:14px;">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));</span><strong style="font-size:24px;">
    </strong><span style="font-size:14px;">      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 > len)
          len1 = len;
        memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
        len -= len1;
        stream += len1;
        audio_buf_index += len1;
      }
    }</span>

    当我们获取一些数据后,立即返回查看是否需要从队列中获得更多的数据,或者是否已经这样做过。如果我们仍然有很多包需要处理,保存留作后面处理。最后就可以释放packet。


    我们得到从main read loop 读入到 queue 中的音频数据,然后被 audio_callback 函数读取,发送到 SDL,SDL接着链接声卡。编译指令如下:

    gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \
    `sdl-config --cflags --libs`

    视频仍然播放很快,但是音频是按时播放的。这是因为音频信息有一个采样率,我们尽可能快的抽取音频信息,but the audio simply plays from that stream at its leisure according to the sample rate.

    The method of queueing up audio and playing it using a separate thread worked very well: it made the code more managable and more modular. Before we start syncing the video to the audio, we need to make our code easier to deal with.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值