Este artigo foi traduzido por máquina.

Fator DirectX

Uma introdução aos objetos de processamento de áudio

Charles Petzold

Baixar o código de exemplo

Charles PetzoldO componente XAudio2 DirectX é muito mais do que apenas uma maneira de reproduzir sons e música em um aplicativo do Windows 8. Eu vim para vê-lo como um conjunto de construção versátil de processamento de som. Através do uso de instâncias múltiplas IXAudio2SourceVoice e IXAudio2SubmixVoice, programadores podem dividir sons em pipelines separados para processamento personalizado e depois combiná-los para mesclar no IXAudio2MasteringVoice final.

Como demonstrei na edição anterior desta coluna (msdn.microsoft.com/magazine/dn198248), XAudio2 permite filtros de áudio padrão aplicar a fonte e submix vozes. Estes filtros atenuam as faixas de freqüência e, consequentemente, alteram o conteúdo harmônico e timbre dos sons.

Mas muito mais poderosa é uma instalação generalizada que fornece acesso para os fluxos de áudio reais, passando por vozes. Você simplesmente pode analisar este fluxo de áudio, ou modificá-lo.

Esta facilidade é conhecida informalmente como um efeito de"áudio". Mais formalmente, ela envolve a criação de um áudio de processamento objeto (APO), também conhecido como cross-platform APO ou XAPO, quando ele pode ser usado com aplicativos de Xbox 360 como Windows.

XAudio2 inclui dois APOs predefinidos para tarefas comuns. A função XAudio2CreateVolumeMeter cria uma APO que permite que um programa obter dinamicamente o pico de amplitude de um fluxo de áudio em intervalos convenientes para a aplicação. O XAudio­2CreateReverb função cria uma APO que se aplica a eco ou reverberação para uma voz com base em 23 parâmetros de tempo de execução — e pelo "runtime" neste contexto quer dizer parâmetros que podem ser alterados dinamicamente enquanto o APO está processando ativamente o áudio. Além disso, uma biblioteca, conhecida como XAPOFX fornece efeitos de eco e reverberação, bem como um limitador de volume e um equalizador de quatro bandas.

Uma APO implementa a interface IXAPO, e uma APO com parâmetros de tempo de execução implementa a interface IXAPOParameters. Mas uma abordagem mais fácil para criar seus próprio APOs envolve deriv­ing das classes CXAPOBase e CXAPOParametersBase, que implementam essas interfaces e lidar com grande parte da sobrecarga.

Decorrentes dessas duas classes é a estratégia que vou usar neste artigo. Além dos outros arquivos de cabeçalho e bibliotecas importantes que eu tenho discutido nas colunas anteriores, projetos que implementam APOs precisam de uma referência para o arquivo de cabeçalho xapobase.h e a biblioteca de importação de xapobase.lib.

Usando objetos de processamento de áudio

Antes de discutir as partes internas de uma classe de APO, deixe-me mostrar-lhe como aplicar os efeitos de vozes XAudio2. O projeto de SimpleEffectDemo no código para download para esta coluna permite que você carregar um arquivo de biblioteca de música do Windows 8 e jogá-lo. É semelhante ao código mostrado nas colunas anteriores: O arquivo é carregado e decodificada usando classes Media Foundation e tocou com XAudio2. SimpleEffectDemo cria apenas duas vozes XAudio2: uma voz de fonte para gerar o áudio e a voz de masterização necessária que o áudio para o hardware de som de decantação.

SimpleEffectDemo também contém dois não parametrizados CXAPO­derivados, chamados OneSecondTremoloEffect (que se aplica um tremolo, ou duvidando de volume, com base em simples modulação de amplitude) e OneSecondEchoEffect de Base. Figura 1 mostra o programa em um arquivo de música carregado. Cada um dos dois efeitos é ativada ou desabilitada por um ToggleButton. A imagem mostra habilitado para o efeito de eco, mas o efeito de tremolo desabilitado.

The SimpleEffectDemo Program with Two Audio Effects
Figura 1 o programa de SimpleEffectDemo com dois efeitos de áudio

APOs podem ser aplicados a qualquer tipo de voz XAudio2. Quando aplicado a vozes de fonte ou submix vozes, o processamento de áudio ocorre depois do built-in filtros que você definir com SetFilterParameters, mas antes os filtros aplicados ao áudio enviado para outras vozes usando SetOutputFilterParameters.

Eu escolhi para aplicar estes dois efeitos para a voz de masterização. O código que cria os efeitos e anexa-los para a voz de masterização é mostrado em Figura 2. Cada efeito é referenciado com um XAUDIO2_­EFFECT_DESCRIPTOR. Se houver mais de um efeito (como é o caso aqui), você usar uma matriz de estruturas. Que estrutura (ou matriz), em seguida, referenciado por uma estrutura de XAUDIO2_EFFECT_CHAIN, que é passada para o método de SetEffectChain, apoiado por todas as vozes XAudio2. A ordem dos efeitos é importante: Neste caso, o efeito de eco vai ter um fluxo de áudio que já tem o efeito de tremolo aplicado.

Figura 2 aplicando dois efeitos de áudio para uma voz de masterização

 

// Create tremolo effect 
ComPtr<OneSecondTremoloEffect> pTremoloEffect = new OneSecondTremoloEffect(); 
// Create echo effect 
ComPtr<OneSecondEchoEffect> pEchoEffect = new OneSecondEchoEffect(); 
// Reference those effects with an effect descriptor array 
std::array<XAUDIO2_EFFECT_DESCRIPTOR, 2> effectDescriptors; 
effectDescriptors[0].pEffect = pTremoloEffect.Get(); 
effectDescriptors[0].InitialState = tremoloToggle->IsChecked->Value; 
effectDescriptors[0].OutputChannels = 2; 
effectDescriptors[1].pEffect = pEchoEffect.Get(); 
effectDescriptors[1].InitialState = echoToggle->IsChecked->Value; 
effectDescriptors[1].OutputChannels = 2; 
// Reference that array with an effect chain 
XAUDIO2_EFFECT_CHAIN effectChain; 
effectChain.EffectCount = effectDescriptors.size(); 
effectChain.pEffectDescriptors = effectDescriptors.data(); 
hresult = pMasteringVoice->SetEffectChain(&effectChain); 
if (FAILED(hresult))
  throw ref new COMException(hresult, "pMasteringVoice->SetEffectChain failure");

Após a chamada de SetEffectChain, as instâncias de efeito não devem ser relacionadas mais pelo programa. XAudio2 já adicionada uma referência a essas instâncias e o programa pode liberar suas próprias cópias, ou ComPtr pode fazer isso por você. A partir daqui, os efeitos são identificados por índices — neste caso 0, para o efeito de tremolo e 1 para o efeito de eco. Você pode querer usar uma enumeração para aqueles constantes.

Para ambos os efeitos, eu configurei o campo de InitialState do XAUDIO2_­EFFECT_DESCRIPTOR para um ToggleButton verificar status. Isso determina se o efeito inicialmente está habilitado ou desabilitado. Os efeitos são mais tarde habilitados e desabilitados o Checked e Unchecked manipuladores para os dois controles ToggleButton, conforme mostrado no Figura 3.

Figura 3 Habilitando e desabilitando efeitos de áudio

void MainPage::OnTremoloToggleChecked(Object^ sender, 
  RoutedEventArgs^ args)
{
  EnableDisableEffect(safe_cast<ToggleButton^>(sender), 0);
}
void MainPage::OnEchoToggleChecked(Object^ sender, 
  RoutedEventArgs^ args)
{
  EnableDisableEffect(safe_cast<ToggleButton^>(sender), 1);
}
void MainPage::EnableDisableEffect(ToggleButton^ toggle, int index)
{
  HRESULT hresult = toggle->IsChecked->Value ? 
    pMasteringVoice->EnableEffect(index) :
  pMasteringVoice->DisableEffect(index);
  if (FAILED(hresult))
    throw ref new COMException(hresult, "pMasteringVoice->Enable/DisableEffect " +
       index.ToString());
}

Instanciação e inicialização

OneSecondTremoloEffect e OneSecondEchoEffect derivam de CXAPOBase. Talvez a primeira perplexidade que você encontrará ao derivar dessa classe é lidar com o construtor de CXAPOBase. Esse construtor exige um ponteiro para uma estrutura XAPO_REGISTRATION_PROPERTIES inicializado, mas como essa estrutura se inicializado? C++ requer que um construtor de classe base completo antes de qualquer código na classe derivada é executado.

Isso é um bocado de um dilema, que pode ser resolvido definindo e inicializar a estrutura como uma variável global, ou um campo estático, ou dentro de um método estático. Prefiro a abordagem do campo estático neste caso, como você pode ver no arquivo de cabeçalho OneSecondTremoloEffect.h no Figura 4.

Figura 4 o arquivo de cabeçalho OneSecondTremoloEffect.h.

#pragma once
class OneSecondTremoloEffect sealed : public CXAPOBase
{
private:
  static const XAPO_REGISTRATION_PROPERTIES RegistrationProps;
  WAVEFORMATEX waveFormat;
  int tremoloIndex;
public:
  OneSecondTremoloEffect() : CXAPOBase(&RegistrationProps),
                             tremoloIndex(0)
  {
  }
protected:
  virtual HRESULT __stdcall LockForProcess(
    UINT32 inpParamCount,
    const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS  *pInpParams,
    UINT32 outParamCount,
    const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS  *pOutParam) override;
  virtual void __stdcall Process(
    UINT32 inpParameterCount,
    const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParams,
    UINT32 outParamCount,
    XAPO_PROCESS_BUFFER_PARAMETERS *pOutParams,
    BOOL isEnabled) override;
};
class __declspec(uuid("6FB2EBA3-7DCB-4ADF-9335-686782C49911"))
                       OneSecondTremoloEffect;

O campo RegistrationProperties é inicializado no arquivo de código (chegando em breve). Um ponteiro para ele é passado para o construtor de CXAPOBase. Muitas vezes um derivado de CXAPOBase também irá definir um campo do tipo WAVEFORMATEX (como este faz) ou WAVEFORMATEXTENSIBLE (em geral) para o formato de forma de onda do áudio fluxo passando através do efeito da economia.

Observe também o declspec ("especificador de declaração") na parte inferior do arquivo que associa um GUID de classe OneSecondTremoloEffect. Você pode gerar um GUID para suas próprias classes de efeitos da opção de criar o GUID no menu ferramentas no Visual Studio.

Um derivado de CXAPOBase deve substituir o método de processo e geralmente substitui o método de LockForProcess também. O método de LockForProcess permite o APO executar a inicialização com base em um determinado formato de áudio, que inclui a taxa de amostragem, o número de canais e o tipo de dados de amostra. O método do processo realmente executa a análise ou a modificação dos dados de áudio.

Figura 5 mostra esses dois métodos, bem como a inicialização do campo RegistrationProperties. Observe que o primeiro campo de XAPO_REGISTRATION_PROPERTIES é identificada com a classe GUID.

Figura 5 o arquivo OneSecondTremoloEffect.cpp

#include "pch.h"
#include "OneSecondTremoloEffect.h"
const XAPO_REGISTRATION_PROPERTIES OneSecondTremoloEffect::RegistrationProps =
{
  __uuidof(OneSecondTremoloEffect),
  L"One-Second Tremolo Effect",
  L"Coded by Charles Petzold",
  1,      // Major version number
  0,      // Minor version number
  XAPOBASE_DEFAULT_FLAG | XAPO_FLAG_INPLACE_REQUIRED,
  1,      // Min input buffer count
  1,      // Max input buffer count
  1,      // Min output buffer count
  1       // Max output buffer count
};
HRESULT OneSecondTremoloEffect::LockForProcess(
  UINT32 inpParamCount,
  const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS  *pInpParams,
  UINT32 outParamCount,
  const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS  *pOutParams)
{
  waveFormat = * pInpParams[0].pFormat;
  return CXAPOBase::LockForProcess(inpParamCount, pInpParams,
                                   outParamCount, pOutParams);
}
void OneSecondTremoloEffect::Process(UINT32 inpParamCount,
  const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParams,
  UINT32 outParamCount,
  XAPO_PROCESS_BUFFER_PARAMETERS *pOutParams,
  BOOL isEnabled)
{
  XAPO_BUFFER_FLAGS flags = pInpParams[0].BufferFlags;
  int frameCount = pInpParams[0].ValidFrameCount;
  const float * pSrc = static_cast<float *>(pInpParams[0].pBuffer);
  float * pDst = static_cast<float *>(pOutParams[0].pBuffer);
  int numChannels = waveFormat.nChannels;
  switch(flags)
  {
  case XAPO_BUFFER_VALID:
    for (int frame = 0; frame < frameCount; frame++)
    {
      float sin = 1;
      if (isEnabled)
      {
        sin = fabs(DirectX::XMScalarSin(DirectX::XM_PI * tremoloIndex /
                                        waveFormat.nSamplesPerSec));
        tremoloIndex = (tremoloIndex + 1) % waveFormat.nSamplesPerSec;
      }
      for (int channel = 0; channel < numChannels; channel++)
      {
        int index = numChannels * frame + channel;
        pDst[index] = sin * pSrc[index];
      }
    }
    break;
  case XAPO_BUFFER_SILENT:
    break;
  }
  pOutParams[0].ValidFrameCount = pInpParams[0].ValidFrameCount;
  pOutParams[0].BufferFlags = pInpParams[0].BufferFlags;
}

Em teoria, APOs podem lidar com vários buffers de entrada e vários buffers de saída. No entanto, APOs são atualmente restritas a um buffer de entrada e saída de um buffer. Essa restrição afeta os últimos quatro campos da estrutura XAPO_REGISTRATION_PROPERTIES e os parâmetros para o método de LockForProcess e processo. Para ambos os métodos, inpParamCount e outParamCount são sempre iguais a 1, e os argumentos de ponteiro sempre apontam para uma instância da estrutura indicada.

À taxa de 100 chamadas por segundo, o método de processo de uma APO recebe um buffer de entrada de dados de áudio e prepara um buffer de saída. É possível que APOs realizar conversões de formato — por exemplo, para alterar a taxa de amostragem entre a entrada e o buffer de saída ou o número de canais, ou o tipo de dados das amostras.

Essas conversões de formato podem ser difícil, então você pode indicar o sexto campo da estrutura XAPO_REGISTRATION_PROPERTIES que conversões, você não está preparado para implementar. O XAPOBASE_DEFAULT_FLAG indica que você não deseja realizar conversões de taxa de amostragem, o número de canais, os tamanhos de bits de amostra ou os tamanhos de quadro (o número de amostras em cada chamada de processo).

O formato dos dados áudio passando através da APO está disponível a partir de parâmetros para a substituição de LockForProcess sob a forma de uma padrão WAVEFORMATEX estrutura. Comumente, LockForProcess é chamado somente uma vez. APOs a maioria precisam saber a taxa de amostragem e número de canais, e é melhor generalizar seu APO para quaisquer valores possíveis.

Também fundamental é o tipo de dados das amostras-se. Mais frequentemente quando se trabalha com XAudio2, você está lidando com amostras que são inteiros de 16 bits ou valores de ponto flutuante de 32 bits. Internamente, no entanto, XAudio2 prefere usar dados de ponto flutuante (tipo Flutuar C++), e que é o que você verá em suas APOs. Se você gostar, você pode verificar o tipo de dados de exemplo no método LockForProcess. No entanto, é também minha experiência que o campo wFormatTag das WAVEFORMATEX estrutura não é igual a WAVE_FORMAT_IEEE_FLOAT como seria de esperar. Em vez disso, é WAVE_FORMAT_EXTENSIBLE (o valor 65534), que significa que você está realmente lidando com uma estrutura WAVEFORMATEXTENSIBLE, em que caso o campo de SubFormat indica o tipo de dados KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.

Se o método de LockForProcess encontrar um formato de áudio não pode tratar, ele deve retornar um HRESULT indicando um erro, talvez E_NOTIMPL para indicar "não implementado".

Processamento dos dados de áudio

O método LockForProcess pode gastar o tempo que ele precisa para a inicialização, mas o método de processo é executado no thread de processamento de áudio, e ele não deve te disperses. Você vai descobrir que para uma taxa de amostragem de 44.100 Hz, o campo de ValidFrameCount dos iguais parâmetros buffer 441, indicando que o processo é chamado de 100 vezes por segundo, cada vez com 10 ms de dados de áudio. Para estéreo de dois canais, a reserva contém 882 valores float com os canais intercalados: canal esquerdo seguido por canal direito.

O campo BufferFlags é XAPO_BUFFER_VALID ou XAPO_BUFFER_SILENT. Esse sinalizador permite ignorar se não houver nenhum dado de áudio real vem através de processamento. Além disso, o parâmetro isEnabled indica se este efeito foi habilitado através dos métodos EnableEffect e DisableEffect que você já viu.

Se a reserva é válida, os loops de APO OneSecondTremoloEffect entre os quadros e os canais, calcula um índice para o buffer, e transferências flutuam valores do buffer fonte (pSrc) para o buffer de destino (pDst). Se o efeito for desativado, é aplicado um factor de 1 para os valores de origem. Se ele estiver habilitado, um valor de seno é aplicado, calculado usando a função de XMScalarSin a zippy da biblioteca matemática de DirectX.

No final do método de processo, o ValidFrameCount e BufferFlags são definidas sobre a estrutura de parâmetros de saída para os valores correspondentes da estrutura de parâmetros de entrada.

Embora o código trata os buffers de entrada e saídos como objetos separados, isso não é realmente o caso. Entre as bandeiras que você pode definir a estrutura de XAPO_REGISTRATION_PROPERTIES são XAPO_FLAG_INPLACE_SUPPORTED (que está incluído no XAPOBASE_DEFAULT_FLAG) e XAPO_FLAG_INPLACE_REQUIRED. A palavra "local" significa que os ponteiros para os buffers de entrada e saídos — chamado pSrc e pDst no meu código — são realmente iguais. Não há um único buffer usado para entrada e saída. Você definitivamente deve estar ciente do fato de que ao escrever seu código.

Mas cuidado: É minha experiência que, se esses sinalizadores são removidos, separados buffers são de fato presentes, mas apenas o buffer de entrada é válido para ambos de entrada e saída.

Salvar Exemplos do passado

O efeito de tremolo apenas precisa alterar amostras. Um efeito de eco precisa salvar amostras anteriores porque a saída de um efeito de eco de um segundo é o atual audio plus áudio a partir de um segundo atrás.

Isso significa que a classe de OneSecondEchoEffect precisa manter seu próprio buffer de dados de áudio, que ele define como um vetor do tipo float e tamanhos durante o método de LockForProcess:

delayLength = waveFormat.nSamplesPerSec;   

int numDelaySamples = waveFormat.nChannels * 
                      waveFormat.nSamplesPerSec;delayBuffer.resize(numDelaySamples);

Esse vetor de delayBuffer é suficiente para manter um segundo de dados de áudio, e é tratada como um amortecedor rotativo. O método LockForProcess Inicializa o buffer para valores 0 e inicializa um índice para esta reserva:

delayIndex = 0;

Figura 6 mostra o método de processo em OneSecondEchoEffect. Porque o efeito de eco deve continuar após concluída a fonte de áudio, você já não pode ignorar processamento quando o sinalizador XAPO_BUFFER_SILENT não indica nenhuma entrado de áudio. Em vez disso, depois que o arquivo de som for concluído, a saída de áudio deve continuar a jogar a final do eco. A variável denominada fonte é, portanto, o áudio de entrada ou o valor 0, dependendo a existência da bandeira XAPO_BUFFER_SILENT. Metade desse valor de origem é combinada com a metade do valor armazenado no buffer de atraso, e o resultado é salvo de volta para o buffer de atraso. A qualquer momento, você está ouvindo metade o áudio atual, além de um quarto do áudio de um segundo atrás, além de um oitavo do áudio de há dois segundos e assim por diante. Você pode ajustar o balanço para efeitos diferentes, incluindo um eco que fica mais alto a cada repetição.

Figura 6 o método de processo em OneSecondEchoEffect

void OneSecondEchoEffect::Process(UINT32 inpParamCount,
  const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParams,
  UINT32 outParamCount,
  XAPO_PROCESS_BUFFER_PARAMETERS *pOutParams,
  BOOL isEnabled)
{
  const float * pSrc = static_cast<float *>(pInpParams[0].pBuffer);
  float * pDst = static_cast<float *>(pOutParams[0].pBuffer);
  int frameCount = pInpParams[0].ValidFrameCount;
  int numChannels = waveFormat.nChannels;
  bool isSourceValid = pInpParams[0].BufferFlags == XAPO_BUFFER_VALID;
  for (int frame = 0; frame < frameCount; frame++)
  {
    for (int channel = 0; channel < numChannels; channel++)
    {
      // Get sample based on XAPO_BUFFER_VALID flag
      int index = numChannels * frame + channel;
      float source = isSourceValid ? pSrc[index] : 0.0f;
      // Combine sample with contents of delay buffer and save back
      int delayBufferIndex = numChannels * delayIndex + channel;
      float echo = 0.5f * source + 0.5f * delayBuffer[delayBufferIndex];
      delayBuffer[delayBufferIndex] = echo;
      // Transfer to destination buffer
      pDst[index] = isEnabled ? echo : source;
    }
    delayIndex = (delayIndex + 1) % delayLength;
  }
  pOutParams[0].BufferFlags = XAPO_BUFFER_VALID;
  pOutParams[0].ValidFrameCount = pInpParams[0].ValidFrameCount;
}

Tente definir o comprimento do buffer de atraso para um décimo de segundo:

delayLength = waveFormat.nSamplesPerSec / 10;

Agora recebe mais de um efeito de reverberação do que um eco distinto. Naturalmente, em um verdadeiro APO, você quererá controle programático sobre estes diversos parâmetros (e outros também), é por isso que o verdadeiro eco/reverb APO é controlado por uma estrutura de XAUDIO2FX_REVERB_PARAMETERS com 23 campos.

Uma APO com parâmetros

APOs a maioria permitem que seu comportamento a ser alterado com os parâmetros de tempo de execução que podem ser definidos programaticamente. O método SetEffectParameters é definido para todas as classes de voz e faz referência a um determinado APO com um índice. Uma APO parametrizado é um pouco complicado para implementar, mas não muito.

Na edição anterior desta coluna, demonstrei como usar o filtro bandpass interno implementado nas vozes XAudio2 fonte e submix criar um 26-equalizador gráfico, em que cada banda afeta um terço de oitava do espectro de áudio total. Esse programa GraphicEqualizer efetivamente dividir o som em 26 peças para a aplicação desses filtros e então recombinados os streams de áudio. Esta técnica pode ter parecido um pouco ineficiente.

É possível implementar um algoritmo de equalizador gráfico inteiro em um único APO e obter o mesmo efeito que o programa anterior, com apenas uma voz de fonte e uma voz de masterização. Isso é o que eu fiz no programa GraphicEqualizer2. O novo programa tem a mesma aparência e soa o mesmo que o programa anterior, mas internamente é bem diferente.

Um dos problemas em passar parâmetros para uma APO é a sincronização de threads. O método Process é executado no segmento de processamento de áudio e parâmetros provavelmente estão sendo definidos do thread UI. Felizmente, a classe de CXAPOParametersBase realiza essa sincronização para você.

Você precisa primeiro definir uma estrutura para os parâmetros. Para o efeito de 26-band equalizador, a estrutura contém um campo que é uma matriz de 26 níveis de amplitude:

struct OneThirdOctaveEqualizerParameters
{
  std::array<float, 26> Amplitude;};

Dentro do programa, os membros dessa matriz são calculados a partir os valores de decibéis dos controles deslizantes.

Para inicializar o CXAPOParametersBase, você precisa passar uma matriz de três estruturas de parâmetro para o construtor. CXAPOParametersBase usa este bloco de memória para executar a sincronização de threads.

Mais uma vez encontramos o problema de passar inicializado dados para um construtor de classe base de classe derivada. A solução que eu escolhi desta vez foi para definir o construtor de classe derivada como protegido e instanciar a classe de um método estático público chamado criar, que é mostrado no Figura 7.

Figura 7 o estático criar método para OneThirdOctaveEqualizerEffect

OneThirdOctaveEqualizerEffect * OneThirdOctaveEqualizerEffect::Create()
{
  // Create and initialize three effect parameters
  OneThirdOctaveEqualizerParameters * pParameterBlocks =
    new OneThirdOctaveEqualizerParameters[3];
  for (int i = 0; i < 3; i++)
    for (int band = 0; band < 26; band++)
      pParameterBlocks[i].Amplitude[band] = 1.0f;
  // Create the effect
  return new OneThirdOctaveEqualizerEffect(
    &RegistrationProps,
    (byte *) pParameterBlocks,
    sizeof(OneThirdOctaveEqualizerParameters),
    false);
}

Os filtros digitais biquad implementados no XAudio2 (que são emulados nesta APO) envolvem a seguinte fórmula:

y = (b0·x + b1·x’ + b2·x’’ – a1·y’ – a2·y’’) / a0

Nesta fórmula, x é a entrada amostra, x' é o exemplo de entrada anterior e x ' é a amostra antes disso. A saída é o y, y' é a saída anterior e y ' é a saída antes que.

Um efeito equalizador assim precisa salvar dois valores de entrada anteriores para cada canal, e dois anteriores valores para cada canal e cada banda de saída.

As seis constantes nesta fórmula dependem do tipo de filtro; a frequência de corte (ou, no caso de um filtro passa-banda, a freqüência central) em relação a taxa de amostragem; e Q, a qualidade do filtro. Para um equalizador gráfico de uma terceira oitavas, cada filtro tem um Q correspondente a uma largura de banda de um terço de oitava, ou 4.318. Cada banda tem um conjunto exclusivo de constantes que são calculadas no método LockForProcess com o código mostrado na Figura 8.

Figura 8 o cálculo das constantes de filtro equalizador

Q = 4.318f;       // One-third octave
static float frequencies[26] =
{
  20.0f, 25.0f, 31.5f, 40.0f, 50.0f, 63.0f, 80.0f, 100.0f, 125.0f,
  160.0f, 200.0f, 250.0f, 320.0f, 400.0f, 500.0f, 630.0f, 800.0f, 1000.0f,
  1250.0f, 1600.0f, 2000.0f, 2500.0f, 3150.0f, 4000.0f, 5000.0f, 6300.0f
};
for (int band = 0; band < 26; band++)
{
  float frequency = frequencies[band];
  float omega = 2 * 3.14159f * frequency / waveFormat.nSamplesPerSec;
  float alpha = sin(omega) / (2 * Q);
  a0[band] = 1 + alpha;
  a1[band] = -2 * cos(omega);
  a2[band] = 1 - alpha;
  b0[band] = Q * alpha;       // == sin(omega) / 2;
  b1[band] = 0;
  b2[band] = -Q * alpha;      // == -sin(omega) / 2;
}

Durante o método de processo, a APO Obtém um ponteiro para a estrutura atual de parâmetros com uma chamada para CXAPOParametersBase::BeginProcess, lançando neste caso o valor de retorno para uma estrutura do tipo um terço­OctaveEqualizerParameters. No final do método de processo, uma chamada para CXAPOParametersBase::EndProcess libera a preensão do método sobre a estrutura de parâmetros. O método do processo completo é mostrado na Figura 9.

Figura 9 o método de processo em OneThirdOctaveEqualizerEffect

void OneThirdOctaveEqualizerEffect::Process(UINT32 inpParamCount,
  const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParam,
  UINT32 outParamCount,
  XAPO_PROCESS_BUFFER_PARAMETERS *pOutParam,
  BOOL isEnabled)
{
  // Get effect parameters
  OneThirdOctaveEqualizerParameters * pEqualizerParams =
    (OneThirdOctaveEqualizerParameters *) CXAPOParametersBase::BeginProcess();
  // Get buffer pointers and other information
  const float * pSrc = static_cast<float *>(pInpParam[0].pBuffer);
  float * pDst = static_cast<float *>(pOutParam[0].pBuffer);
  int frameCount = pInpParam[0].ValidFrameCount;
  int numChannels = waveFormat.nChannels;
  switch(pInpParam[0].BufferFlags)
  {
  case XAPO_BUFFER_VALID:
    for (int frame = 0; frame < frameCount; frame++)
    {
      for (int channel = 0; channel < numChannels; channel++)
      {
        int index = numChannels * frame + channel;
        // Do very little if filter is disabled
        if (!isEnabled)
        {
          pDst[index] = pSrc[index];
          continue;
        }
        // Get previous inputs
        float x = pSrc[index];
        float xp = pxp[channel];
        float xpp = pxpp[channel];
        // Initialize accumulated value
        float accum = 0;
        for (int band = 0; band < 26; band++)
        {
          int bandIndex = numChannels * band + channel;
          // Get previous outputs
          float yp = pyp[bandIndex];
          float ypp = pypp[bandIndex];
          // Calculate filter output
          float y = (b0[band] * x + b1[band] * xp + b2[band] * xpp
                                  - a1[band] * yp - a2[band] * ypp) / a0[band];
          // Accumulate amplitude-adjusted filter output
          accum += y * pEqualizerParams->Amplitude[band];
          // Save previous output values
          pypp[bandIndex] = yp;
          pyp[bandIndex] = y;
        }
        // Save previous input values
        pxpp[channel] = xp;
        pxp[channel] = x;
        // Save final value adjusted for filter gain
        pDst[index] = accum / Q;
      }
    }
    break;
  case XAPO_BUFFER_SILENT:
    break;
  }
  // Set output parameters
  pOutParam[0].ValidFrameCount = pInpParam[0].ValidFrameCount;
  pOutParam[0].BufferFlags = pInpParam[0].BufferFlags;
  CXAPOParametersBase::EndProcess();
}

Uma característica da programação que eu sempre gostei é que problemas muitas vezes tem várias soluções. Às vezes uma solução diferente é mais eficiente, de alguma forma e às vezes não. Certamente, substituir 26 ocorrências de IXAudio2SubmixVoice com um único APO é uma mudança radical. Mas se você acha que essa alteração reflecte-se no desempenho muito melhor, você está errado. O Gerenciador de tarefa do Windows 8 revela que os dois programas de GraphicEqualizer são aproximadamente equivalentes, sugerindo que não dividir o fluxo de áudio em 26 vozes submix é tão louco afinal.

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 aos seguintes especialistas técnicos pela revisão deste artigo: Duncan McKay (Microsoft) e James McNellis (Microsoft)