Ogg Vorbis, OpenAL e double buffering para áudio

Num outro blog e neste post falei algo sobre OpenAL. Claro que lá eu falava sobre a manipulação de sons em 3D, mas o OpenAL pode ser usado como uma alternativa mais simples para lidarmos com o driver de áudio. Tudo o que temos que fazer é criar buffers, preenche-los com dados PCM, criar uma fonte de áudio e executar um play. Aqui vou te mostrar como fazer double buffering com o OpenAL e também como usar a biblioteca libvorbis (mais especificamente a libvorbisfile) para decodificar arquivos Ogg Vorbis.

Como vamos lidar com OpenAL para tocar áudio estereo sem nos preocuparmos com a localização espacial, os buffers podem ser preenchidos com PCM estéreo que a biblioteca os colocará nos canais normalmente. O problema com o OpenAL é que, se tivermos PCM estéreo e tentarmos criar um listener, colocando-o em outra coordenada que não seja a origem, a biblioteca não conseguirá fazê-lo…. Para lidar com áudio 3D os buffers precisam ter dados monofônicos… Mas, como eu disse, não estamos preocupados com isso aqui.

Ao invés de decodificar todo arquivo ogg vorbis e colocarmos os dados PCM em um grande buffer, temos que decodificá-lo em pedaços e colocando os pedaços da stream PCM em pequenos buffers e ir tocando-os até que todo o arquivo tenha sido decodificado. Podemos fazer isso usando apenas um buffer, mas há um problema: Quando o buffer terminar de ser tocado haverá um pequeno delay entre a decodificação, preenchimeto e o play. Isso causará, na melhor das hipóteses, um “click” na música… E não queremos isso, não é?

Uma solução é usarmos dois buffers. Enquanto um está sendo tocado o outro estará sendo preenchido… Assim, quando o OpenAL terminar de tocar um buffer ele continuará tocando o outro… e assim por diante. Isso pode ser feito com as funções alSourceQueueBuffers()alSourceUnqueueBuffers(). A primeira cria uma fila de buffers e os associa a uma fonte de áudio. A segunda, retira um ou mais buffers da fila. Outra função útil é alGetSourcei(), usando o parâmetro AL_BUFFERS_PROCESSED. Assim podemos obter a quantidade de buffers já processados que estão na fila… Eis um código de exemplo, usando a libvorbisfile (e a libopenal e libalut):

#include <stdlib.h>
#include <stdio.h>
#include <AL/alut.h>
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>

/* Por que 2 buffers? Porque usarei double buffering aqui.
   Enquanto um buffer é tocado o outro fica carregado e
   na espera.

   Podemos usar triple buffering ou mais até... mas,
   double buffering é suficiente. */
#define MAX_OPENAL_BUFFERS 2

/* O tamanho do buffer contendo o PCM decodificado 
   (32 kB é mais que suficiente!). */
#define MAX_PCM_BUFFER_SIZE 32*1024

int main (int argc, char **argv)
{
  OggVorbis_File vf;
  ALuint source;
  ALuint buffers[MAX_OPENAL_BUFFERS];
  ALuint released[MAX_OPENAL_BUFFERS];
  ALint count;
  FILE *fp;
  int i, current_section;
  long pos, ret;
  static char pcmout[MAX_PCM_BUFFER_SIZE];
  int eof = 0;

  if (!*++argv)
  {
    fprintf(stderr, "Uso: vorbisplay <oggfile>\n");
    return 1;
  }

  if ((fp = fopen(*argv, "rb")) == NULL)
  {
    fprintf(stderr, "Erro abrindo arquivo '%s'\n", *argv);
    return 1;
  }

  if (ov_open_callbacks(fp, &vf, NULL, 0, OV_CALLBACKS_DEFAULT) < 0)
  {
    fprintf(stderr, "Arquivo não parece ser um OGG VORBIS.\n");
    fclose(fp);
    return 1;
  }

  vorbis_info *vi = ov_info(&vf, -1);
  printf("Sampling rate: %d, Canais: %d\n", vi->rate, vi->channels);

  /* Tenta criar e carregar o PCM inicial de 2 buffers. */
  alutInit(&argc, argv);
  alGenBuffers(MAX_OPENAL_BUFFERS, buffers);

  /* Temos apenas uma fonte de audio. */
  alGenSources(1, &source);

  /* Carrega os buffers com os streams iniciais,
     decodificados. */
  for (i = 0; i < MAX_OPENAL_BUFFERS; ++i)
  {
    pos = 0;

    while (pos < MAX_PCM_BUFFER_SIZE)
    {
      /* Decodifica para o buffer no formato S16LE. */
      if ((ret = ov_read(&vf, 
                         pcmout + pos, 
                         MAX_PCM_BUFFER_SIZE - pos, 
                         0, 2, 1, 
                         &current_section)) < 0)
      {
        fclose(fp);
        fprintf(stderr, "Erro decodificando stream.\n");
        return 1;
      }

      if (ret == 0)
      {
        eof = 1;
        break;
      }

      pos += ret;
    }

    /* Manda o stream PCM para um buffer do
       OpenAL. */
    alBufferData(buffers[i], AL_FORMAT_STEREO16, pcmout, pos, vi->rate);
  }

  /* Enfilera os buffers e começa a tocar... */
  alSourceQueueBuffers(source, MAX_OPENAL_BUFFERS, buffers);
  alSourcePlay(source);

  if (alGetError())
  {
    fclose(fp);
    fprintf(stderr, "Error playing with OpenAL.\n");
    return 1;
  }

  /* Enquanto não chegamos ao final do arquivo... */
  while (!eof)
  {
    /* Pega a quantidade de buffers já processada e os handles */
    alGetSourcei(source, AL_BUFFERS_PROCESSED, &count);
    alSourceUnqueueBuffers(source, count, released);

    /* Se tem buffers já processados, decodifica e carrega no buffer
       livre. */
    for (i = 0; i < count; ++i)
    {
      pos = 0;

      while (pos < MAX_PCM_BUFFER_SIZE)
      {
        if ((ret = ov_read(&vf, 
                           pcmout + pos, 
                           MAX_PCM_BUFFER_SIZE - pos, 
                           0, 2, 1, 
                           &current_section)) < 0)
        {
          fclose(fp);
          fprintf(stderr, "Erro decodificando stream.\n");
          return 1;
        }

        if (ret == 0)
        {
          eof = 1;
          break;
        }

        pos += ret;
      }

      /* Manda stream PCM para o novo buffer. */
      alBufferData(released[i], AL_FORMAT_STEREO16, pcmout, pos, vi->rate);
    }

    /* Enfilera o buffer preenchido. */
    alSourceQueueBuffers(source, count, released);

    /* Espera 50 ms para tentar de novo... só para não
       matar a CPU de fome. */
    alutSleep(50.0);
  }

  /* Ok, acabou a música. Fecha a loja e apaga a luz. */
  alutExit();
  fclose(fp);

  return 0;
}

Coloque o seu fonte de ouvido e teste o programinha com:

$ gcc -O3 -mtune=native -o vorbisplay vorbisplay.c -lopenal -lalut -lvorbisfile
$ ./vorbisplay music.ogg

Note que o código cria 2 buffers (poderiam ser 3, 4, …), os preenche com os dados decodificados por ov_read(), coloca os buffers numa fila associada à fonte de áudio e chama alSourcePlay(). No outro loop ele fica verificando se existem buffers já processados. Se houver um, ele desinfileira o buffer processado, o preenche e o re-enfileira… E continua fazendo isso até que a música termine!

A chamada a alutSleep() só é necessária (e poderia muito bem ser usleep()) para não causar muita carga na CPU (CPU indo a 100% de uso!)… Provavelmente as funções alGetSourcei e alSourceUnqueueBuffers não são pontos de cancelamento de threads ou não realizam syscalls, assim, enquanto não houver buffers processados, o loop ficará “em vazio” e o kernel pode achar que temos um alto consumo de CPU… O valor de 50 ms foi escolhido arbitrariamente, tentando levar em conta que podem ocorrer task switches (tipicamente uma task switch pode ocorrer a cada 20 ms, nos sistemas modernos).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s