Áudio em baixo nível: Usando ALSA

Áudio é o que resulta do deslocamento mecânico do diafragma de um alto-falante, mas do ponto de vista de software, ele nada mais é do que uma sequência de valores que são enviados para um convertor Digital-Analógico. Para efeitos de explicação considerarei a saída de audio estéreo, apenas. Ou seja: dois canais. O do lado esquerdo e o do lado direito.

As placas de som funcionam mais ou menos assim: Uma rotina envia valores de 16 bits para cada um dos lados. Um valor para o lado esquerdo, seguido de um valor para o lado direito. Há um delay “pentelhesimal” entre as duas amostras, mas isso é imperceptível para nós, humanos (talvez um morcego consiga perceber… sei lá!). Por que 16 bits? Por que outra característica de nossos ouvidos (e de nosso cérebro) é que não conseguimos perceber muito bem as variações de amplitude… Não é à toa que a graduação de volume no seu aparelho de som seja exponencial… A cada vez que você aumenta o volume em uma unidade, está multiplicando a amplitude por 10 (por isso, também, a medida de intensidade sonora é medida em deciBel [dB]).

Quando a placa de som recebe esses valores, eles são convertidos para tensões na saída da placa, proporcionais aos valores. Suponha que a saída de um dos canais de placa de som tenha amplitude entre 0 e 1 V. Com 16 bits, onde 0 corresponde a 0 V e 65535 corresponde a 1 V, temos steps de cerca de 15 ㎶, ou seja, cada incremento de 1 no valor corresponde a um incremento de 15 ㎶ na saída do canal de áudio.

Então, sabemos agora que, para obter o som estéreo, temos que enviar 16 bits para o lado esquerdo e, logo depois, 16 bits para lado direito. Mas devemos fazer isso numa velocidade controlada, constante. Cada par de valores deve ser enviado para a placa de som numa frequência exata de 44100 pares a cada segundo, para garantir a mesma qualidade de CD… Essa taxa é chamada de sampling rate (literalmente, “taxa de amostragem”), que não deve ser confundida com bit rate, que você já pode ter ouvido falar, quando lida com arquivos MP3… Só por curiosidade, o bit rate é uma taxa de transmissão de um stream compactado e não tem qualquer sentido falarmos de bit rate num stream PCM.

Embora nossos ouvidos e cérebros não percebem muito bem a variação de amplitude, conseguimos perceber muito bem a variação de frequência. Quanto maior a frequência de amostragem, mais aproximados da forma de onda original do áudio teremos na saída. A figura abaixo ilustra o que é conhecido como PCM (Pulse Coded Modulation), que corresponde aos valores discretos da amplitude e pontos de amostragem, de um sinal “sem sinal” (apenas valores positivos):

Pulse Coded Modulation de uma senóide.
Pulse Coded Modulation de uma senóide.

Note como a área cinza aproxima-se da curva vermelha, mas não é exatamente a mesma… Se diminuirmos o espaçamento entre as amostras (no eixo X) continuaremos tendo esse “efeito escada”, mas a área cinza seria ainda mais aproximada da curva original… Como nossos ouvidos conseguem perceber, teóricamente, ondas nas frequẽncias entre 20 ㎐ e 20 ㎑. Na prática, quando a frequẽncia chega a perto de 12 ㎑ já não conseguimos perceber coisa alguma… A indústria resolveu adotar o padrão de 44.1 ㎑ como taxa de amostragem para cada canal. Isso mais que garante a fidelidade de nossa percepção, mesmo para aqueles que conseguem perceber frequências acima da faixa dos 12 ㎑ (que é o MEU caso, por exemplo!)… Ainda, segundo a teoria da amostragem, a frequência de amostragem deve ser de, pelo menos, o dobro da frequência máxima desejada. A frequência de 44.1 ㎑ é mais que o dobre do máximo perceptível, mesmo pelos macaquinhos que adoram um funk (magoou?).

Áudio é, então, uma sequência de valores de 16 bits dispostos em um buffer “entrelaçado”, onde o primeiro valor corresponde ao canal esquerdo e o segundo ao direito… o terceiro, ao esquerdo e o quarto ao direito… e assim por diante.

No caso do ALSA, tudo o que temos que fazer para “tocar” um buffer é “abrir” o dispositivo default (o da placa de som), ajustar alguns valores para o driver (sampling rate, canais, tipo de amostra), alocar espaço para o buffer, preenchẽ-lo e enviar o conteúdo para o driver… Isso pode ser feito de maneira síncrona, assim:

/*
   playback.c

   Este programinha lê um arquivo PCM e o toca,
   via ALSA.

   Compile com: gcc -o playback playback.c -lasound
*/
#include <stdio.h>

/* Usa a API mais recente para ALSA. */
#define ALSA_PCM_NEW_HW_PARAMS_API
#include <alsa/asoundlib.h>

#ifndef min
#define min(a,b) ((a) < (b) ? (a) : (b))
#endif

/* Vou usar 1024 "frames" como exemplo.
   Cada "frame" é um par esquerda/direita de 16 bits. */
#define MAX_FRAMES 1024

int main(int argc, char *argv[])
{
  int fd, rc, size, dir;
  unsigned int val;
  snd_pcm_t *handle;
  snd_pcm_hw_params_t *params;
  snd_pcm_uframes_t frames, buffsize;
  char *buffer;

  /* O arquivo foi informado? */
  if (!*++argv)
  {
    fprintf(stderr, "Usage: playback <rawfile>\n");
    return 1;
  }

  /* Tenta abrir o arquivo. */
  if ((fd = open(*argv, O_RDONLY)) == -1)
  {
    fprintf(stderr,
            "Error opening raw audio file '%s'.\n",
            *argv);
    return 1;
  }

  /* Abre o dispositivo default para playback. */
  if ((rc = snd_pcm_open(&handle, "default",
                         SND_PCM_STREAM_PLAYBACK,
                         0)) < 0)
  {
    close(fd);
    fprintf(stderr,
            "unable to open pcm device: %s\n",
            snd_strerror(rc));
    return 1;
  }

  /* Aloca espaço para a estrutura snd_pcm_hw_params
     e pega os valores default. */
  snd_pcm_hw_params_alloca(&params);
  snd_pcm_hw_params_any(handle, params);

  /* === Ajusta os parâmetros desejados... === */
  /* Modo entrelaçado. Cada frame contém o sample esquerdo,
     seguido do direito. */
  snd_pcm_hw_params_set_access(handle, params,
    SND_PCM_ACCESS_RW_INTERLEAVED);

  /* Samples no formato S16LE (16 bits, com sinal e
     little-endian). */
  snd_pcm_hw_params_set_format(handle, params,
    SND_PCM_FORMAT_S16_LE);

  /* Dois canais (stereo) */
  snd_pcm_hw_params_set_channels(handle, params, 2);

  /* Sample Rate de 44.1 kHz */
  val = 44100;
  snd_pcm_hw_params_set_rate_near(handle, params,
    &val, &dir);

  /* Ao ajustar um período para 1024 frames, estamos dizendo
     ao ALSA para usar buffers que comportem 2048 amostras
     (ou seja, 4 kB). É pouco, mas o hardware não suporta
     buffers muito grandes! */
  snd_pcm_get_params(handle, &buffsize, &frames);
  printf("ALSA reports maximum of %lu frames. "
         "Using %u frames.\n", frames, MAX_FRAMES);
  frames = min(frames, MAX_FRAMES);
  snd_pcm_hw_params_set_period_size_near(handle, params,
    &frames, &dir);

  /* Ajusta od novos parâmetros no driver. */
  if ((rc = snd_pcm_hw_params(handle, params)) < 0)
  {
    close(fd);
    snd_pcm_close(handle);
    fprintf(stderr,
      "unable to set hw parameters: %s\n",
      snd_strerror(rc));
    return 1;
  }

  /* Aloca os buffers. */
  snd_pcm_hw_params_get_period_size(params, &frames, &dir);
  size = frames * 4; /* 2 bytes/sample vezes 2 canais. */
  printf("ALSA reports %lu frames per period "
         "after setup.\n", frames);
  buffer = (char *)malloc(size);

  /* Fica no loop até acabarem os blocos... */
  for (;;)
  {
    // Lê um pedaço do audio (4 kB!)...
    if ((rc = read(fd, buffer, size)) <= 0)
      break;

    /* Os últimos frames podem não preencher um buffer
       inteiro! */
    frames = rc / 4;
    if ((rc = snd_pcm_writei(handle, buffer, frames)) < 0)
    {
      if (rc == -EPIPE)
      {
        /* EPIPE significa 'underrun' */
        fprintf(stderr, "underrun occurred\n");
        snd_pcm_prepare(handle);
      }
      else
        fprintf(stderr,
          "error from writei: %s\n", snd_strerror(rc));
      break;
    }
  }

  snd_pcm_drain(handle);  /* É como se fosse um flush(). */
  snd_pcm_close(handle);

  free(buffer);
  close(fd);

  puts("Ok. Acabou!");

  return 0;
}

O programinha acima (que é auto-explicativo) lê um arquivo no formato PCM, gerado para conter amostras sinalizadas de 16 bits, em estéreo, a uma taxa de 44100 ㎐ de sampling rate. Para criar tais arquivos usando o ffmpeg a partir de um MP3 ou qualquer outro formato que contenha áudio, você pode usar o script audio2pcm, abaixo:

#!/bin/bash

if [ $# -ne 1 ]; then
  echo Usage: audio2pcm \<audiofile\>.
  exit 1
fi

ffmpeg -i "$1" -f s16le -c:a pcm_s16le \
  -ar 44100 -ac 2 "${1%.*}.pcm"

Resolvi usar valores sinalizados caso precise traçar um gráfico ou passar esses valores por uma rotina FFT (Fast Fourier Transform) para criar um daqueles bar graphs que todo mundo gosta de ver…

Para usar o programa, converta um arquivo MP3 (ou qualquer outro com áudio) e “toque-o” com o playback:

$ ./audio2pcm musica.mp3
$ ./playback musica.pcm

Como falei antes, o programinha faz o playback de forma síncrona. A função snd_pcm_writei() é bloquada até que o ALSA tenha espaço para acomodar outro conjunto de 1024 frames. Um programinha que toca buffers de forma assíncrona é só um pouco mais complicado, mas nem tanto… Envolve a adição de um callback. Mas, isso eu deixo para outro artigo…

A intenção aqui é mostrar como é simples lidar com áudio de forma mais direta. Neste artigo você pode aprender a usar o OpenAL para lidar, além de áudio em formato PCM, com posicionamento 3D!

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