Projeto Virtual Worlds

Computação Gráfica 3D em C

Nem só de gráficos vive o 3D

Até agora tenho postado informações a respeito do uso do OpenGL para apresentar modelos tridimensionais. Como nem só de gráficos vive uma simulação, eis algumas informações sobre áudio em 3D… Provavelmente você já sabe que o ‘GL’, do OpenGL, significa Graphics Library. Vou mostrar aqui um pouco sobre o OpenAL (AL, de Audio Library).

No que se refere à biblioteca, seu computador tem um ou mais audio devices. Ao invés de falarmos “placa de som”, um device é mais genérico: Podemos usar um arquivo como um device (a saída do áudio será, por exemplo, um arquivo MP3, ao invés dos alto-falantes do seu headphone). Ainda, em teoria, você pode lidar com mais de um device. Na prática, você terá apenas um dispositivo.

Cada device suporta um ou mais contextos… Um contexto é um container de estados associados a um device. No caso do OpenAL, um contexto contém um listener, uma ou mais fontes de áudio e um ou mais buffers…

O Listener é o objeto relacionado ao ouvinte (você). Existem, ainda, 2 objetos que você pode criar à vontade (existem limites, claro!): Sources e Buffers. Onde, source é uma “fonte de áudio” e buffer é onde os dados do áudio são armazenados. Esse isolamento permite que duas fontes de áudio compartilhem um único buffer, por exemplo. Em resumo, temos um device, que possui um contexto, que incorpora um listener e um ou mais sources que, por sua vez, estão atrelados a buffers.

Tanto o listener quanto os sources são posicionados no espaço 3D através de coordenadas X,Y e Z, com os eixos orientados pela regra da mão direita (como no OpenGL):

Right Hand

Mas, diferente do OpenGL, não há múltiplos espaços vetoriais, apenas um que chamarei daqui por diante de sound space, para não confundir com os espaços vetoriais do OpenGL. Isso quer dizer que não precisamos nos preocupar com coordenadas homogêneas, por exemplo. O posicionamento dos objetos é feito de uma maneira bem simples:

alListener3f(AL_POSITION, x, y, z);

alSource3f(source, AL_POSITION, x, y, z);

Aqui, a variável source é um inteiro sem sinal, do tipo ALuint, que é “alocado” pela função alGetSources(), bem parecido com algumas funções do OpenGL:

ALuint source;

alGetSources(1, &source);

Veremos depois que um buffer é “alocado” de forma semelhante.

Além do posicionamento, tanto o listener quanto os sources têm conjuntos de atributos que podem ser alterados. O listener, por exemplo, tem o atributo AL_ORIENTATION que especifica dois vetores perpendiculares que apontam, respectivamente, para frente e para cima do ponto onde está posicionado o ouvinte. Isso permite à biblioteca definir onde estão seus ouvidos direito e esquerdo. Provavelmente não é necessário que ambos os vetores sejam perpendiculares e/ou que sejam unitários (é provável que OpenAL os normalize e “acerte” o vetor up com base no vetor fornecido e o vetor frontal, do mesmo jeito que a antiga função gluLookAt(), do OpenGL, fazia). Mesmo que isso seja verdade (não testei), é sempre bom dar uma mãozinha à biblioteca… Você poderá usar a dica da função RecalcUpVector citada aqui.

O listener têm os seguintes atributos: AL_GAIN, AL_POSITION, AL_VELOCITY e AL_ORIENTATION. O atributo AL_GAIN pode ser interpretado como um volume “master”. AL_VELOCITY (tanto do listener quanto dos sources) é usado nas rotinas que lidam com efeito doppler (que não cobrirei aqui, por enquanto).

Já para os sources, ajustar a posição e associar um buffer ao source parece ser o suficiente, mas os seguintes atributos estão disponíveis: AL_GAIN, AL_MIN_GAIN, AL_MAX_GAIN, AL_POSITION, AL_VELOCITY, AL_DIRECTION, AL_SOURCE_RELATIVE, AL_REFERENCE_DISTANCE, AL_MAX_DISTANCE, AL_ROLLOFF_FACTOR, AL_CONE_INNER_ANGLE, AL_CONE_OUTER_ANGLE, AL_PITCH, AL_LOOPING, AL_MSEC_OFFSET, AL_BYTE_OFFSET, AL_SAMPLE_OFFSET, AL_BUFFER, AL_SOURCE_STATE, AL_BUFFER_QUEUED, AL_BUFFERS_PROCESSED. A quantidade de atributos é maior porque queremos poder alterar mais as características das fontes do que as do listener. Alguns atributos são fáceis de entender:

AL_SOURCE_RELATIVE é um atributo do tipo TRUE ou FALSE. Se estiver ligado (AL_TRUE), significa que a posição da fonte de áudio é relativa ao listener. Tome o exemplo de fontes de ouvido: Você pode mexer a cabeça de um lado para outro, levantar e abaixar, que as posições relativa dos fontes continuarão as mesmas (eles se movem junto com a sua cabeça). Ao dizer que o source tem posição relativa, você informa que o vetor em AL_POSITION do source é sempre relativa ao AL_POSITION do listener, caso contrário, o source será colocado no sound space em relação à origem do sistema de coordenadas (0,0,0).

Por padrão, os sources são omni-direcionais. Isso é, não têm direção. Mas podemos direcionar o áudio usando o atributo AL_DIRECTION. Ele é um vetor (unitário, de preferência) que aponta para a direção em que o cone de áudio estará apontando. AL_CONE_INNER_ANGLE e AL_CONE_OUTER_ANGLE especificam o cone interno (100% do ganho) e o cone externo (atenuação, interpolada linearmente, até total silêncio), como mostrado abaixo:

openal3dcone2-4

Mas, o atributo mais importante dos sources é AL_BUFFER. Ele aceita um valor inteiro sem sinal que contém o identificador do buffer que contém os samples do áudio. Para tanto, é necessário criar um objeto buffer, carregá-lo e associá-lo ao source:

ALuint buffer;
void *data;
ALuint size, freq;

alGetBuffers(1, &buffer);
alBufferData(buffer, AL_MONO16, data, size, freq);
alSourcei(source, AL_BUFFER, buffer);

A função alBufferData aloca e inicializa um objeto buffer. Isso é necessário porque algumas placas de áudio possuem memória interna, como as antigas SoundBlaster AWE32 e a Gravis Ultrasound. Isso permite a aceleração de hardware, se houver alguma. Uma vez que o buffer foi criado e preenchido, associamos com o source usando o atributo AL_BUFFER, como mostrado acima.

No exemplo que mostrarei aqui, usei uma função da biblioteca ALUT (OpenAL UTilities) que carrega um áudio de um arquivo e cria o buffer: alutCreateBufferFromFile().

Uma informação importante é que as fontes de áudio são são capazes de posicionarem o áudio se esses forem MONO. OpenAL suporta buffers com áudio STEREO, mas não permite que sejam posicionados… Isso é útil quando você quer uma música de fundo: Posicione uma simples fonte de áudio no centro do sound space, associe-a com um buffer com a música STEREO, mude o AL_GAIN da fonte para um valor mais baixo que 1.0 e, voilà… musica de fundo. Se quiser música STEREO, ou com múltiplos canais, posicionada, precisará criar fontes isoladas para cada canal e posicioná-las onde quiser no sound space.

Quando o listener e os sources (lembre-se podem haver diversos!) forem posicionados, orientados, direcionados, …, basta mandar o OpenAL tocar os sources com alSourcePlay() ou alSourcePlayv(). O primeiro inicia um único source e o segundo permite o início de um array de sources.

Eis um exemplo… O arquivo wilhelm.wav está disponível no github (aqui), no meu sandbox, junto com este código e o Makefile, no diretório tests/openal:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

// Precisa do libopenal-dev e libalut-dev instalados.
// Ou o equivalente dessas libs, se for Windows.
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alut.h>

int main(int argc, char *argv[])
{
  ALCdevice *device;
  ALCcontext *context;
  ALuint source;
  ALuint buffer;
  ALenum source_state;
  ALfloat listenerOrientation[] = { 0, 0, -1,   // Para Frente.
                                    0, 1,  0 }; // Para cima

  // Posições da fonte de áudio.
  ALfloat positions[] = { 0,  0,  1,   // Atrás
                          0,  0, -1,   // Na frente
                          1,  0,  0,   // Direita
                         -1,  0,  0 }; // Esquerda
  int i;

  // alutCreateBufferFromFile() precisa que o alut seja inicializado.
  // É preciso iniciar ALUT sem inicializar o contexto.
  if (alutInitWithoutContext(&argc, argv) == AL_FALSE)
  {
    fprintf(stderr, "Error initializing ALUT.\n");
    return 1;
  }

  // Pega o dispositivo default.
  if ((device = alcOpenDevice(NULL)) == NULL)
  {
    alutExit();
    fprintf(stderr, "Erro inicializando dispositivo.\n");
    return 1;
  }

  // cria o contexto default para o dispositivo.
  context = alcCreateContext(device, NULL);

  if ((context == NULL) || (alcMakeContextCurrent(context) == AL_FALSE))
  {
    fprintf(stderr, "Erro ajustando contexto.\n");
    alcCloseDevice(device);
    alutExit();
    return 1;
  }

  // Ajusta o listener.
  alListener3f(AL_POSITION, 0, 0, 0);
  alListenerfv(AL_ORIENTATION, listenerOrientation);

  // Cria source.
  alGenSources(1, &source);

  // Cria buffer e carrega áudio.
  // wilhelm.wav é um áudio em MONO16.
  if ((buffer = alutCreateBufferFromFile("./wilhelm.wav")) == 0)
  {
    alDeleteSources(1, &source);
    alcMakeContextCurrent(NULL);
    alcCloseDevice(device);
    alutExit();
    fprintf(stderr, "Error creating buffer.\n");
    return 1;
  }

  // Liga buffer ao source.
  alSourcei(source, AL_BUFFER, buffer);

  // Tenta tocar nas 4 posições.
  for (i = 0; i != 4; i++)
  {
    printf("Playing sample at position (%.1f, %.1f, %.1f)\n",
           positions[3 * i], positions[3 * i + 1], positions[3 * i + 2]);

    // Ajusta a posição do source e toca-o.
    alSourcefv(source, AL_POSITION, &positions[3 * i]);
    alSourcePlay(source);

    // Fica no loop, enquanto estiver tocando.
    do
    {
      alGetSourcei(source, AL_SOURCE_STATE, &source_state);
    } while (source_state == AL_PLAYING);

    sleep(1);
  }

  // Seja educado e limpe tudo antes de sair!
  alSourcei(source, AL_BUFFER, 0);
  alDeleteBuffers(1, &buffer);
  alDeleteSources(1, &source);
  alcMakeContextCurrent(NULL);
  alcDestroyContext(context);
  alcCloseDevice(device);
  alutExit();

  return 0;
}

Uma pequena observação:

Não há necessidade de esperar pelo término do áudio para ajustar novos parâmetros para o listener ou o source. O loop interno, que verifica o status do source, só está ai por conveniência, para ouvirmos o áudio em momentos distintos.

A documentação do OpenAL 1.1 (a mais recente, atualmente) pode ser encontrada aqui.

UPDATE

O código abaixo serve apenas para mostrar informações sobre a biblioteca OpenAL instalada no seu sistema:

// Compilar com:
// gcc -O3 -o alinfo alinfo.c -lopenal
//
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <AL/al.h>
#include <AL/alc.h>

int main(int argc, char *argv[])
{
  ALCdevice *device;
  ALCcontext *context;
  const char *version, *renderer, *vendor, *extensions;
  char *e, *p;

  if ((device = alcOpenDevice(NULL)) == NULL)
  {
    fprintf(stderr, "ERRO ao tentar abrir dispositivo.\n");
    return 1;
  }

  if ((context = alcCreateContext(device, NULL)) == NULL)
  {
    alcCloseDevice(device);
    fprintf(stderr, "ERRO ao tentar criar contexto default.\n");
    return 1;
  }

  alcMakeContextCurrent(context);

  version = alGetString(AL_VERSION);
  renderer = alGetString(AL_RENDERER);
  vendor = alGetString(AL_VENDOR);
  extensions = alGetString(AL_EXTENSIONS);

  printf("Version: %s\n"
         "Renderer: %s\n"
         "Vendor: %s\n"
         "Extensions:\n",
         version, renderer, vendor);

  e = strdup(extensions);

  p = strtok(e, " ");

  while (p)
  {
    printf(" %s\n", p);
    p = strtok(NULL, " ");
  }

  free(e);

  alcMakeContextCurrent(NULL);
  alcDestroyContext(context);
  alcCloseDevice(device);

  return 0;
}

Em um dos ambientes em que tenho para testes, o código acima cuspiu a seguinte saída:

$ ./alinfo
Version: 1.1 ALSOFT 1.14
Renderer:
OpenAL Soft
Vendor:
OpenAL Community
Extensions:
AL_EXT_ALAW
AL_EXT_DOUBLE
AL_EXT_EXPONENT_DISTANCE
AL_EXT_FLOAT32
AL_EXT_IMA4
AL_EXT_LINEAR_DISTANCE
AL_EXT_MCFORMATS
AL_EXT_MULAW
AL_EXT_MULAW_MCFORMATS
AL_EXT_OFFSET
AL_EXT_source_distance_model
AL_LOKI_quadriphonic
AL_SOFT_buffer_samples
AL_SOFT_buffer_sub_data
AL_SOFTX_deferred_updates
AL_SOFT_direct_channels
AL_SOFT_loop_points

Procure informações no Google sobre essas extensões…

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