Este artigo foi traduzido por máquina.

Fator DirectX

Geração de Som do Windows 8 com XAudio2

Charles Petzold

Baixar o código de exemplo

Charles PetzoldUma app Store do Windows para Windows 8 pode reproduzir MP3 ou arquivos de som WMA facilmente usando o MediaElement — você simplesmente dar um URI ou um fluxo para o arquivo de som. Apps da loja do Windows também podem acessar a jogar a API para streaming de vídeo ou áudio para dispositivos externos.

Mas se você precisa de mais sofisticadas de processamento de áudio? Talvez você gostaria de modificar o conteúdo de um arquivo de áudio no seu caminho para o hardware, ou gerar sons dinamicamente.

A app Store do Windows também pode executar estes trabalhos através de DirectX. Windows 8 oferece suporte a dois componentes do DirectX para processamento e geração de som — Core Audio e XAudio2. No grande esquema das coisas, ambos são interfaces de nível bastante baixo, mas é inferior a XAudio2 Core Audio e é voltada mais para aplicativos que exigem uma conexão mais estreita com o hardware de áudio.

XAudio2 é o sucessor do DirectSound e a biblioteca do Xbox XAudio, e é provavelmente sua melhor aposta para apps da loja do Windows que precisa fazer coisas interessantes com som. Enquanto XAudio2 destina-se principalmente para jogos, que não deve nos impedir de usá-lo para fins mais graves — como fazer música ou entreter o usuário de negócios com sons engraçados.

XAudio2 versão 2.8 é um componente interno do Windows 8. Como o resto do DirectX, a interface de programação de XAudio2 baseia-se em COM. Embora seja teoricamente possível para acesso XAudio2 de qualquer linguagem de programação suportada pelo Windows 8, a linguagem mais natural e mais fácil para XAudio2 é C++. Trabalho com som muitas vezes requer código de alto desempenho, portanto C++ é uma boa opção nesse sentido também.

Um primeiro programa XAudio2

Vamos começar a escrever um programa que usa XAudio2 para jogar um simples segundo 5 som em resposta ao impulso de uma tecla. Porque você pode ser o novo Windows 8 e DirectX programação, eu vou levá-la um pouco lento.

Eu vou assumir que você tem uma versão do Visual Studio instalado que é adequada para a criação de aplicativos Windows Store. Na caixa de diálogo New Project, selecione Visual C++ e Windows Store no esquerdo e em branco App (XAML) na lista de modelos disponíveis. Eu dei meu projeto o nome de SimpleAudio, e você pode encontrar esse projeto entre o código de download para este artigo.

Na construção de um executável que usa XAudio2, você precisará vincular o programa com a biblioteca de importação de xaudio2.lib. Abrir a caixa de diálogo de propriedades do projeto, selecionando o último item no menu de projeto, ou clicando no nome do projeto no Solution Explorer e selecionando Propriedades. Na coluna da esquerda, selecione Propriedades de configuração e, em seguida, vinculador e entrada. Clique em Additional Dependencies (o item superior) e a setinha. Selecione Editar e digite xaudio2.lib na caixa.

Você também quer uma referência para o arquivo de cabeçalho xaudio2.h, então adicione a seguinte instrução para a lista de cabeçalhos pré-compilados em pch.h:

 

#include <xaudio2.h>

O arquivo MainPage XAML, adicionei um TextBlock para exibir todos os erros que o programa pode encontrar trabalhando com a API de XAudio2 e um botão para reproduzir um som. Essas linhas são mostradas na Figura 1.

Figura 1 o arquivo MainPage XAML para SimpleAudio

<Page x:Class="SimpleAudio.MainPage" ...
>   <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">     <TextBlock Name="errorText"                FontSize="24"                TextWrapping="Wrap"                HorizontalAlignment="Center"                VerticalAlignment="Center" />     <Button Name="submitButton"             Content="Submit Audio Button"             Visibility="Collapsed"             HorizontalAlignment="Center"             VerticalAlignment="Center"             Click="OnSubmitButtonClick" />   </Grid> </Page>

A maior parte do arquivo de cabeçalho MainPage.xaml.h é mostrada em Figura 2. Eu removi a declaração do método OnNavigatedTo porque eu não vou usá-lo. O manipulador Click do botão é declarado, como são quatro campos relacionados com a utilização do programa de XAudio2.

Figura 2 o arquivo de cabeçalho de MainPage.xaml.h para SimpleAudio

namespace SimpleAudio {   public ref class MainPage sealed   {   private:     Microsoft::WRL::ComPtr<IXAudio2> pXAudio2;     IXAudio2MasteringVoice * pMasteringVoice;     IXAudio2SourceVoice * pSourceVoice;     byte soundData[2 * 5 * 44100];   public:     MainPage();   private:     void OnSubmitButtonClick(Platform::Object^ sender,       Windows::UI::Xaml::RoutedEventArgs^ args);   }; }

Qualquer programa que deseja usar XAudio2 deve criar um objeto que implementa a interface IXAudio2. (Você verá como fazer isso em breve). IXAudio2 deriva da classe de IUnknown famoso em COM, e é inerentemente referência-contados, o que significa que ele exclui seus recursos próprios, quando já não é referenciado por um programa. A classe de ComPtr (COM ponteiro) no namespace Microsoft::WRL transforma um ponteiro para um objeto COM um "ponteiro inteligente" que mantém o controle de suas próprias referências. Esta é a abordagem recomendada para trabalhar com os objetos em uma app Store do Windows.

Qualquer programa não-trivial de XAudio2 precisa também ponteiros para objetos que implementam as interfaces IXAudio2MasteringVoice e IXaudio2SourceVoice. No jargão de XAudio2, uma "voz" é um objeto que gera ou modifica dados de áudio. A voz de masterização é conceitualmente um mixer de som que reúne todas as vozes individuais e prepara-los para o hardware de som-geração. Você só tem um desses, mas você pode ter um número de vozes de fonte que geram sons separados. (Há também maneiras de aplicar filtros ou efeitos de vozes de fonte).

Os ponteiros IXAudio2MasterVoice e IXAudio2SourceVoice não são contados de referência; suas vidas são governadas pelo objeto IXAudio2.

Incluí também uma grande variedade de 5 segundos no valor de dados de som:

byte soundData[5 * 2 * 44100];

Em um programa real, você vai querer alocar uma matriz desse tamanho em tempo de execução — e livrar-se dele quando você não precisa dele, mas você verá em breve porque eu fiz isso dessa forma.

Como para calcular esse tamanho de matriz? Embora XAudio2 suporta áudio comprimido, a maioria dos programas que geram o som vai ficar com o formato conhecido como modulação do pulso-código ou PCM. Formas de onda de som em PCM são representadas por valores de tamanho fixo em uma taxa de amostragem fixa. Música em discos compactos, a taxa de amostragem é 44.100 vezes por segundo, com 2 bytes por amostra em estéreo, para um total de 176.400 bytes de dados por 1 segundo de áudio. (Quando incorporando sons em um aplicativo, compressão é recomendado. XAudio2 suporta ADPCM; WMA e MP3 são compatíveis também com o motor de Media Foundation.)

Para este programa, eu também escolhi usar uma taxa de amostragem de 44.100 com 2 bytes por amostra. Em C++, cada amostra é, portanto, um curto. Eu vou ficar com som mono para agora, então 88.200 bytes são necessários por segundo de áudio. Na atribuição de matriz, que é multiplicado por 5 por 5 segundos.

Criando os objetos

Grande parte do arquivo MainPage.xaml.cpp é mostrado em Figura 3. Todos do XAudio2 inicialização é executada no Construtor MainPage. Começa com uma chamada para XAudio2Create para obter um ponteiro para um objeto que implementa a interface IXAudio2. Este é o primeiro passo na utilização XAudio2. Ao contrário de algumas interfaces COM, nenhuma chamada para CoCreateInstance é necessária.

Figura 3 MainPage.xaml.cpp

MainPage::MainPage() {   InitializeComponent();   // Create an IXAudio2 object   HRESULT hr = XAudio2Create(&pXAudio2);   if (FAILED(hr))   {     errorText->Text = "XAudio2Create failure: " + hr.ToString();     return;   }   // Create a mastering voice   hr = pXAudio2->CreateMasteringVoice(&pMasteringVoice);   if (FAILED(hr))   {     errorText->Text = "CreateMasteringVoice failure: " + hr.ToString();     return;   }   // Create a source voice   WAVEFORMATEX waveformat;   waveformat.wFormatTag = WAVE_FORMAT_PCM;   waveformat.
nChannels = 1;   waveformat.
nSamplesPerSec = 44100;   waveformat.
nAvgBytesPerSec = 44100 * 2;   waveformat.
nBlockAlign = 2;   waveformat.wBitsPerSample = 16;   waveformat.cbSize = 0;   hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveformat);   if (FAILED(hr))   {     errorText->Text = "CreateSourceVoice failure: " + hr.ToString();     return;   }   // Start the source voice   hr = pSourceVoice->Start();   if (FAILED(hr))   {     errorText->Text = "Start failure: " + hr.ToString();     return;   }   // Fill the array with sound data   for (int index = 0, second = 0; second < 5; second++)   {     for (int cycle = 0; cycle < 441; cycle++)     {       for (int sample = 0; sample < 100; sample++)       {         short value = sample < 50 ?
32767 : -32768;         soundData[index++] = value & 0xFF;         soundData[index++] = (value >> 8) & 0xFF;       }     }   }   // Make the button visible   submitButton->Visibility = Windows::UI::Xaml::Visibility::Visible; } void MainPage::OnSubmitButtonClick(Object^ sender, RoutedEventArgs^ args) {   // Create a button to reference the byte array   XAUDIO2_BUFFER buffer = { 0 };   buffer.AudioBytes = 2 * 5 * 44100;   buffer.pAudioData = soundData;   buffer.Flags = XAUDIO2_END_OF_STREAM;   buffer.PlayBegin = 0;   buffer.PlayLength = 5 * 44100;   // Submit the buffer   HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer);   if (FAILED(hr))   {     errorText->Text = "SubmitSourceBuffer failure: " + hr.ToString();     submitButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;     return;   } }

Uma vez que o objeto IXAudio2 é criado, os métodos CreateMasteringVoice e CreateSourceVoice obter ponteiros para as outras duas interfaces definidas como campos no arquivo de cabeçalho.

A chamada CreateSourceVoice requer uma WAVEFORMATEX estrutura, que vai ser familiar para qualquer pessoa que já trabalhou com áudio na API do Win32. Essa estrutura define a natureza dos dados áudio que você vai usar para esta voz particular. (Vozes de fonte diferente podem usar formatos diferentes). Para PCM, apenas três números são realmente relevantes: a taxa de amostragem (44.100 neste exemplo), o tamanho de cada amostra (2 bytes ou 16 bits) e o número de canais (1 aqui). Os outros campos são baseados nestes: O campo de nBlockAlign é nChannels vezes wBitsPerSample dividida por 8 e campo nAvgBytesPerSec é o produto do nSamplesPerSec e nBlockAlign. Mas, neste exemplo, mostrei a todos os campos com valores explícitos.

Uma vez que o objeto IXAudio2SourceVoice é obtido, o método pode ser chamado sobre ele. Neste ponto, XAudio2 conceitualmente está jogando, mas nós realmente não ter dado qualquer dados de áudio para jogar. Um Stop método também está disponível, e um programa real usaria estes dois métodos para o controle quando sons devem e não devem ser jogando.

Todos os quatro dessas chamadas não são tão simples que aparecem neste código! Todos eles têm argumentos adicionais, mas convenientes padrões são definidos e escolhi simplesmente aceitar esses padrões para agora.

Praticamente todas as chamadas de função e método no DirectX retornam valores HRESULT para indicar sucesso ou falha. Há diferentes estratégias para lidar com esses erros. Eu escolhi simplesmente para exibir o código de erro usando o TextBlock definido no arquivo XAML e parar de processar mais adicional.

Dados de áudio PCM

Construtor conclui enchendo acima soundData matriz com dados de áudio, mas a matriz não é realmente bifurcou-se sobre para o IXAudio2SourceVoice até que o botão é pressionado.

Som é vibração, e os seres humanos são sensíveis a vibrações no ar aproximadamente no intervalo de 20 hertz (ou ciclos por segundo) a 20.000 Hz. O dó médio no piano é aproximadamente 261.6 Hz.

Suponha que você está trabalhando com uma taxa de amostragem de 44.100 Hz e amostras de 16-bit e você deseja gerar dados de áudio para uma forma de onda com uma frequência de 4.410 Hz, que é apenas para além da chave mais alta em um piano. Cada ciclo de uma forma de onda requer 10 amostras de valores de 16 bits assinados. Esses 10 valores seriam repetidos vezes 4.410 para cada segundo de som.

Uma forma de onda com uma frequência de 441 Hz — muito perto de 440 Hz, correspondente à acima C médio usado como um padrão de ajuste — é processado com 100 amostras. Esse ciclo seria repetido vezes 441 para cada segundo de som.

Porque PCM envolve uma taxa de amostragem constante, sons de baixa freqüência parecem ser amostrado e processado em uma resolução muito maior do que os sons de alta freqüência. Isso não é um problema? Não uma forma de onda de 4.410 Hz processada com apenas 10 amostras tem uma quantidade considerável de distorção em comparação com a forma de onda de 441 Hz?

Acontece que qualquer distorção de quantização em PCM ocorre em freqüências superiores a metade da taxa de amostragem. (Isso é conhecido como a freqüência de Nyquist Bell Labs engenheiro Harry Nyquist.) Uma razão, que uma frequência de amostragem de 44.100 Hz foi escolhida para CD de áudio é que a freqüência de Nyquist é 22.050 Hz, e audição humana atinge o máximo cerca de 20.000 Hz. Em outras palavras, a uma taxa de amostragem de 44.100 Hz, a distorção de quantização é inaudível para os seres humanos.

O programa de SimpleAudio gera uma forma de onda através de algoritmos simples — uma onda quadrada com frequência de 441 Hz. Existem 100 amostras por ciclo. Em cada ciclo o primeiro 50 são máximos valores positivos (32.767 ao lidar com inteiros curtos), sendo o próximo 50 máximos valores negativos (-32.768). Observe que esses valores curtos devem ser armazenados na matriz de byte com o byte baixo primeiro:

soundData[index + 0] = value & 0xFF; soundData[index + 1] = (value >> 8) & 0xFF;

Até agora, nada realmente jogou. Isso acontece no manipulador Click do botão. A estrutura XAUDIO2_BUFFER é usada para fazer referência a matriz de bytes com uma contagem de bytes e uma duração especificada como o número de amostras. Esse buffer é passado para o método SubmitSourceBuffer do objeto IXAudio2SourceVoice. Se o método já foi chamado (como tem neste exemplo) então o som começa a jogar imediatamente.

Eu suspeito que eu não tenho que mencionar que o som é reproduzido assincronamente. A chamada SubmitSourceBuffer retorna imediatamente, enquanto um thread separado é dedicado para o processo real de pá de dados para o hardware de som. O XAUDIO2_BUFFER passado para SubmitSourceBuffer podem ser descartados após a chamada — como é este programa quando o manipulador Click é encerrado e a variável local sai do escopo — mas a real matriz de bytes deve permanecer na memória acessível. Na verdade, o programa pode manipular esses bytes como o som é tocado. No entanto, há muito melhores técnicas (envolvendo métodos de retorno de chamada) que permitem que seu programa gerar dinamicamente os dados de som.

Sem usar um callback para determinar quando o som estiver concluída, este programa precisa manter a matriz soundData para a duração do programa.

Você pode pressionar o botão várias vezes, e cada chamada efetivamente filas de outra reserva para ser jogado quando o buffer anterior termina. Se o programa se move para o fundo, o som é silenciado, mas continua a jogar em silêncio. Em outras palavras, se você clicar o botão e move o programa para o fundo pelo menos 5 segundos, nada vai jogar quando o programa retorna para o primeiro plano.

As características do som

Grande parte do som que ouvimos na vida diária vem simultaneamente de uma variedade de fontes diferentes e, portanto, é bastante complexo. No entanto, em alguns casos — e particularmente quando se trata de sons musicais — tons individuais podem ser definidos com poucas características:

  • Amplitude, que é interpretado pelos nossos sentidos, como volume.
  • Freqüência, que é interpretada como breu.
  • Espaço, que pode ser imitado na reprodução de áudio com vários alto-falantes.
  • Timbre, que está relacionada com a mistura dos tons em um som e representa a diferença percebida entre um trompete e um piano, por exemplo.

O projeto de SoundCharacteristics demonstra estas quatro características isoladamente. Ele mantém a taxa de amostragem de 44.100 e amostras de 16-bit do projeto SimpleAudio, mas gera o som em estéreo. Para dois canais de som, os dados devem ser intercalados: uma amostra de 16 bits para o canal esquerdo, seguido de uma amostra de 16 bits para o canal direito.

O arquivo de cabeçalho de MainPage.xaml.h para SoundCharacteristics define algumas constantes:

static const int sampleRate = 44100; static const int seconds = 5; static const int channels = 2; static const int samples = seconds * sampleRate;

Ele também define quatro matrizes para dados de som, mas estas são do tipo curto em vez de byte:

short volumeSoundData[samples * channels]; short pitchSoundData[samples * channels]; short spaceSoundData[samples * channels]; short timbreSoundData[samples * channels];

Usando arrays curtos facilita a inicialização porque os valores de forma de onda de 16 bits não precisam ser quebrado ao meio. Um molde simples permite que a matriz a ser referenciado pelo XAUDIO2_BUFFER, ao apresentar os dados de som. Estas matrizes têm dobro do número de bytes como a matriz em SimpleAudio porque eu estou usando o estéreo neste programa.

Todos os quatro desses conjuntos são inicializados no Construtor MainPage. Para a demonstração de volume, uma onda quadrada de 441 Hz está ainda envolvida, mas ele começa com zero volume, fica progressivamente mais alto durante os primeiros 2 segundos e, em seguida, declina no volume sobre os últimos 2 segundos. Figura 4 mostra o código para inicializar o volumeSoundData.

Figura 4 dados de som que muda em Volume para SoundCharacteristics

for (int index = 0, sample = 0; sample < samples; sample++) {   double t = 1;   if (sample < 2 * samples / 5)     t = sample / (2.0 * samples / 5);   else if (sample > 3 * samples / 5)     t = (samples - sample) / (2.0 * samples / 5);   double amplitude = pow(2, 15 * t) - 1;   short waveform = sample % 100 < 50 ?
1 : -1;   short value = short(amplitude * waveform);   volumeSoundData[index++] = value;   volumeSoundData[index++] = value; }

Percepção humana do volume é logarítmica: Cada duplicação da amplitude de em uma onda equivale a um aumento de 6 dB no volume. (A amplitude de 16 bits usada para áudio de CD tem um alcance dinâmico de 96 decibéis). O código mostrado na Figura 4 alterar o volume primeiro calcula um valor de t que aumenta linearmente de 0 a 1, e depois diminui de volta para 0. A variável de amplitude é calculada usando a função pow e varia de 0 a 32.767. Isso é multiplicado por uma onda quadrada que tem valores de 1 e -1. O resultado é adicionado à matriz duas vezes: em primeiro lugar para o canal esquerdo e, em seguida, para o canal direito.

Percepção humana de freqüência também é logarítmica. Muita da música do mundo organiza passo em todo o intervalo de uma oitava, que é uma duplicação da frequência. As duas primeiras notas do refrão de "Somewhere over the Rainbow" são um salto de oitava, se é cantada por um baixo ou um soprano. Figura 5 mostra o código que varia o tom sobre a escala de duas oitavas: de 220 para 880 com base em um valor de t que (como o exemplo de volume) vai de 0 a 1 e, em seguida, para baixo a 0.

Figura 5 dados de som que muda a freqüência para SoundCharacteristics

double angle = 0; for (int index = 0, sample = 0; sample < samples; sample++) {   double t = 1;   if (sample < 2 * samples / 5)     t = sample / (2.0 * samples / 5);   else if (sample > 3 * samples / 5)     t = (samples - sample) / (2.0 * samples / 5);   double frequency = 220 * pow(2, 2 * t);   double angleIncrement = 360 * frequency / waveformat.
nSamplesPerSec;   angle += angleIncrement;   while (angle > 360)     angle -= 360;   short value = angle < 180 ?
32767 : -32767;   pitchSoundData[index++] = value;   pitchSoundData[index++] = value; }

Nos exemplos anteriores, eu escolhi uma frequência de 441 Hz, porque ele limpa se divide na taxa de amostragem de 44.100. Em geral, a taxa de amostragem não é um múltiplo integral da freqüência, e, portanto, não pode haver um número integral de amostras por ciclo. Em vez disso, este programa mantém uma variável angleIncrement de ponto flutuante que é proporcional à freqüência e usado para aumentar o valor de um ângulo que varia de 0 a 360. Este valor do ângulo é usado para construir a forma de onda.

A demonstração para espaço move o som do centro para o canal esquerdo, à direita, em seguida volta para o centro. Para a demonstração do timbre, a forma de onda começa com uma curva de seno a 441 Hz. Uma curva do seno é a representação matemática do tipo de vibração mais fundamental — a solução da equação diferencial onde a força é inversamente proporcional ao deslocamento. Todas as outras formas de onda periódicas contêm harmônicos, que também são ondas senoidais, mas com freqüências que são múltiplos integrais da freqüência base. A demo de timbre muda a forma de onda sem problemas de uma onda senoidal para uma onda triangular, para uma onda quadrada, uma onda dente de serra, ao aumentar o conteúdo harmônico do som.

A imagem maior

Embora apenas demonstrei como você pode controlar o volume, pitch, espaço e timbre gerando dados de som para um único objeto de IXAudio2SourceVoice, o próprio objeto inclui métodos para alterar o volume e espaço e até mesmo a freqüência. (Uma instalação do espaço "3D" também é suportada). Embora seja possível gerar dados de som composto que combina um monte de tons individuais com uma voz de fonte única, você pode criar vários objetos de IXAudio2SourceVoice e reproduzi-los todos juntos com a mesma voz de masterização.

Além disso, XAudio2 define um IXAudioSubmixVoice que permite que você defina filtros e outros efeitos, tais como a reverberação ou eco. Filtros têm a capacidade de alterar o timbre de tons existentes dinamicamente, o que pode contribuir grandemente para criar sons musicais interessantes e realistas.

Talvez o mais essencial acessório além o que mostrei nesses dois programas requer trabalhar com funções de callback XAudio2. Em vez de alocar e inicializar a grandes blocos de dados de som, como esses dois programas fazem, faz muito mais sentido para um programa para gerar dados de som dinamicamente, como ele está sendo jogado.

Charles Petzold é um colaborador de longa data para MSDN Magazine e autor de "Programação Windows, 6ª edição" (o ' Reilly Media, 2012), um livro sobre como escrever aplicativos para Windows 8. Seu site é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Scott Selfon