Este artigo foi traduzido por máquina.

Fator DirectX

Simulando um sintetizador analógico

Charles Petzold

Baixar o código de exemplo

Charles PetzoldCerca de 50 anos atrás, um físico e engenheiro chamado Robert Moog criaram um sintetizador de música eletrônica com uma característica bastante incomum: um órgão-tipo teclado. Alguns compositores de música eletrônica desacreditada como um dispositivo de controle prosaico e antiquado, enquanto outros compositores — e particularmente os artistas — congratulou-se com este desenvolvimento. No final da década de 1960, Wendy Carlos Switched Bach tornou-se um dos álbuns clássicos mais vendidos de todos os tempos, e o sintetizador Moog tinha entrado no mainstream.

Os primeiros sintetizadores Moog foram programado com cabos de remendo e modular. Em 1970, no entanto, foi lançado o Minimoog — pequeno, fácil de usar e jogar e custa apenas US $1.495. (Uma boa história destes primeiros sintetizadores é o livro, "dias analógicos: A invenção e o impacto do sintetizador Moog"[Harvard University Press, 2004], por Trevor Pinch e Frank Trocco.)

Classificamos o Moog e sintetizadores semelhantes como "analógicos" dispositivos porque eles criam sons usando diferentes tensões geradas a partir do circuito construído a partir de transistores, resistores e capacitores. Em contraste, mais modernos sintetizadores "Digitas" criam som através de cálculos algorítmicos ou amostras digitalizadas. Dispositivos mais antigos ainda são classificados como "subtrativos" sintetizadores: Ao invés de construir um som composto através da combinação de ondas senoidais (uma técnica chamada síntese aditiva), sintetizadores subtractive começam com uma forma de onda rica em harmônicos — como uma onda dente de serra ou quadrada — e, em seguida, executá-lo através de filtros para eliminar alguns harmônicos e alterar o timbre do som.

Um conceito crucial foi pioneiro por Robert Moog foi "controle de tensão". Considere um oscilador, que é o componente de um sintetizador que gera uma onda de áudio básica de algum tipo. Em sintetizadores anteriores, a freqüência desta forma de onda pode ser controlada pelo valor de um resistor em algum lugar no circuito, e esse resistor variável pode ser controlado por um seletor. Mas em um oscilador controlado por tensão (VCO), a freqüência do oscilador é regulada por uma tensão de entrada. Por exemplo, a cada aumento de 1 volt para o oscilador pode freqüência do oscilador dobro. Desta forma, a frequência do VCO pode ser controlada por um teclado que gera uma tensão que aumenta por um volt por oitava.

Em sintetizadores analógicos, a saída de um ou mais VCOs entra em um filtro controlado por tensão (FCR) para alterar o conteúdo harmônico da forma de onda. Tensões de entrada para o FCR controlam a freqüência de corte do filtro, ou a nitidez da resposta do filtro de (o filtro qualidade, ou Q). A saída do FCR então entra em um amplificador controlado por tensão (VCA), o ganho do que é controlado pela tensão de outra.

Geradores de envelope

Mas uma vez que você começa a falar sobre VCFs e VCAs, as coisas ficam complicadas, e um pouco de fundo é necessário.

No século XIX, alguns cientistas (mais notavelmente Hermann von Helmholtz) começaram a fazer incursões significativas para a exploração de tanto a física e a percepção do som. Características do som, tais como a freqüência e a sonoridade que acabou por ser relativamente simples em comparação com o intrincado problema do timbre — que a qualidade do som que permite distinguir um piano de um violino ou trombone. Era a hipótese (e um tanto demonstrada) que o timbre era relacionada com conteúdo harmônico do som, que é os vários graus de intensidade das curvas de seno que constituem o som.

Mas quando o século XX os pesquisadores começaram a investigar ainda mais, eles descobriram que não era tão simples. Conteúdo harmônico muda ao longo de um tom musical, e isto contribui para o timbre do instrumento. Em particular, o início de uma nota de um instrumento musical é crucial para a percepção auditiva. Quando um arco de martelo ou violino piano toca primeiro uma cadeia de caracteres, ou vibrar o ar é impelido para um tubo de metal ou de madeira, ocorre atividade harmônica muito complexa. Esta complexidade diminui muito rapidamente, mas sem ele, tons musicais som maçante e muito menos interessante e distinta.

Para simular a complexidade dos tons musicais de verdade, um sintetizador não pode simplesmente virar um recado e desligar como um interruptor. (Para ouvir um sintetizador tão simples como o som, verifique para fora o programa de ChromaticButtonKeyboard na parcela desta coluna em fevereiro de 2013 msdn.microsoft.com/magazine/jj891059.) No início de cada nota, o som deve ter um breve "blip" de alto volume e timbre diferentes antes de estabilizar. Quando a nota termina, o som deve não simplesmente parar, mas morrer para fora com uma diminuição do volume e complexidade.

Para volume, há um padrão geral para esse processo: Para uma nota tocada num instrumento de seqüência de caracteres, de bronze ou de sopro, o som eleva-se rapidamente a uma intensidade máxima, então morre um pouco e mantém estável. Quando a nota termina, ela diminui rapidamente em volume. Estas duas fases são conhecidas como o "ataque" e "release".

Para instrumentos de percussão mais — incluindo o piano — a nota atinge o volume máximo rapidamente durante o ataque, mas em seguida morre lentamente se o instrumento permanece undamped, por exemplo, enquanto pressiona a tecla de piano. Uma vez que a chave é liberada, a nota rapidamente morre.

Para conseguir estes efeitos, sintetizadores implementam algo chamado um "gerador de envelope." Figura 1 mostra um exemplo razoavelmente padrão chamado um envelope de ataque-decadência-sustentar-lançamento (ADSR). O eixo horizontal é o tempo, e o eixo vertical é a sonoridade.


Figura 1: um Envelope de ataque-decadência-sustentar-lançamento

Quando uma tecla em um teclado e a nota começa a soar, ouvir as seções de ataque e decadência que dão uma explosão de som desde o início, e então a nota estabiliza o nível de sustentar. Quando a chave é liberada e as extremidades de nota, a seção de lançamento ocorre. Para um som de piano-tipo, o tempo de decaimento pode ser uns segundos, e o nível de sustentação é fixado em zero assim o som continua a entrar em decadência, enquanto a tecla é pressionada.

Até mesmo os mais simples sintetizadores analógicos têm dois envelopes ADSR: Um controla o volume e o outro controla o filtro. Este é geralmente um filtro low-pass. Como uma nota é atingida, a freqüência de corte é rapidamente aumentada para permitir mais harmônicos de alta freqüência através de, e em seguida a freqüência de corte diminui um pouco. Enfatizou-se muito, isso cria o distintivo sintetizador analógico chilrear som.

O projeto AnalogSynth

Há nove meses, como eu estava contemplando usando XAudio2 para programar uma simulação digital de um sintetizador analógico de 1970-era pequeno, eu percebi que os geradores do envelope seria um dos aspectos mais desafiadores do trabalho. Não é ainda claro para mim se esses geradores de envelope seria externo para o fluxo de processamento de áudio (e, portanto, acessar os métodos SetVolume e SetFilterParameters de uma voz XAudio2), ou de alguma forma ser construído para o stream de áudio.

Eventualmente, estabeleceu-se na execução os envelopes como efeitos de áudio XAudio2 — mais formalmente conhecido como objetos de processamento de áudio (APOs). Isto significa que a lógica do envelope trabalha diretamente sobre o fluxo de áudio. Tornei-me mais confiante com esta abordagem após codificação lógica de filtro que duplica os filtros biquad digital incorporado XAudio2. Usando meu próprio código do filtro, eu pensei que eu seria capaz de alterar o algoritmo de filtragem no futuro, sem grandes perturbações para a estrutura do programa.

Figura 2 mostra a tela das analógicas resultante­programa de Synth, cuja fonte de código que você pode baixar em archive.msdn.microsoft.com/mag201307DXF. Embora eu era influenciado pelo layout dos controles sobre o Minimoog, eu continuei a interface do usuário real bastante simples, usando, por exemplo, controles deslizantes ao invés de mostradores. A maioria de meu foco era sobre a parte interna.


Figura 2 a tela de AnalogSynth

O teclado é uma série de controles personalizados de chave processamento de eventos de ponteiro e agrupados em controles de oitava. O teclado é na verdade seis oitavas de largura e pode ser rolado horizontalmente usando a grossa listra cinza abaixo as chaves. Um ponto vermelho identifica médio c.

O programa pode tocar 10 notas simultâneas, mas isso é mutável com um simples #define em MainPage.xaml.cs. (Early sintetizadores analógicos, como o Minimoog eram monofônicos). Cada uma destas 10 vozes é uma instância de uma classe chamado de SynthVoice. SynthVoice tem métodos para definir todos os vários parâmetros da voz (incluindo freqüência, volume e envelopes), bem como métodos chamados gatilho e solte para indicar quando uma tecla foi pressionada ou liberada.

O Minimoog alcançado seu som característico de "gorducho" em parte por ter dois osciladores correndo em paralelo e muitas vezes um pouco mistuned, quer deliberadamente ou como resultado da freqüência deriva comum em circuitos analógicos.

Por essa razão, cada SynthVoice cria duas instâncias de uma classe de oscilador, que são controladas a partir do canto superior esquerdo do painel de controle mostrado na Figura 2. O painel de controle permite que você defina a forma de onda e o volume relativo para estes dois osciladores, e poderá transpor a freqüência por uma ou duas oitavas acima ou para baixo. Além disso, você pode compensar a freqüência do oscilador segundo por até meia oitava.

Cada instância de oscilador cria um objeto IXAudio2SourceVoice e expõe os métodos chamados SetFrequency, SetAmplitude e SetWaveform. SynthVoice encaminha as duas saídas de IXAudio2SourceVoice para um IXAudio2SubmixVoice e instancia dois efeitos de áudio personalizados chamados FilterEnvelopeEffect e Amplitude­EnvelopeEffect que se aplica a essa voz submix. Estes dois efeitos compartilham uma classe chamada EnvelopeGenerator que descreverei em breve.

Figura 3 mostra a organização dos componentes em cada SynthVoice. Para os 10 objetos de SynthVoice, há um total de 20 IXAudio2Source­instâncias em 10 casos de IXAudio2SubmixVoice, que em seguida são roteados para um único IXAudio2MasteringVoice de voz. Eu uso uma taxa de amostragem de 48.000 Hz e amostras de ponto flutuante de 32 bits em todo.


Figura 3 a estrutura da classe SynthVoice

O usuário controla o filtro da seção central do painel de controle. Um ToggleButton permite que o filtro para ser ignorada; caso contrário, a freqüência de corte é relativo a nota que está sendo reproduzida. (Em outras palavras, a frequência de corte do filtro controla o teclado.) O Emph­asis slider controla a configuração do Q do filtro. O controle deslizante Envelope controla o grau em que o envelope afeta a frequência de corte do filtro.

Os quatro controles deslizantes associados com o filtro o envelope e o envelope de sonoridade funcionam da mesma forma. Os controles deslizantes de ataque, decaimento e liberação são todas as durações de 10 milissegundos de 10 segundos, em uma escala logarítmica. Os controles deslizantes têm conversores de valor de dica de ferramenta para exibir a duração associada com as configurações.

AnalogSynth não faz com que nenhum ajuste de volume para 20 potenciais simultâneas IXAudio2SourceVoice instâncias de, ou para compensar a tendência de filtros digitais biquad para amplificar o áudio perto da frequência de corte. Consequentemente, AnalogSynth torna mais fácil para sobrecarregar o áudio. Para ajudar o usuário a evitar isso, o programa usa o XAudio2­CreateVolumeMeter função para criar um efeito de áudio que monitora o saída de som. Se o ponto verde no canto superior direito muda para vermelho, saída de áudio está sendo cortada e você deve usar o controle deslizante à direita para diminuir o volume.

Início sintetizadores utilizados fios de patch para conectar componentes. Como resultado deste legado, uma instalação particular sintetizador ainda é conhecida como um "patch". Se você encontrar um patch que faz um som que você deseja manter, pressione o botão salvar e atribuir um nome. Pressione o botão Load para obter uma lista de anteriormente salvo patches e selecione uma. Estes patches (bem como a configuração atual) é armazenada na área de configurações locais.

O algoritmo do gerador de Envelope

Código que implementa um gerador de envelope é basicamente uma máquina de estado, com cinco Estados seqüenciais que chamei Dormant, ataque, Decay, Sustain e Release. Do ponto de vista da interface do usuário, parece mais natural para especificar o ataque, decaimento e sustentar em termos de tempo dura­ções, mas quando realmente executar os cálculos que você precisa convertê-lo em uma taxa — um aumento ou uma diminuição de volume (ou filtro de frequência de corte) por unidade de tempo. Os dois efeitos de áudio em AnalogSynth usam estes níveis de mudanças para implementar o efeito.

Esta máquina de estado não é sempre tão seqüencial como o diagrama no Figura 1 parece implicar. Por exemplo, o que acontece quando uma chave é pressionada e liberada tão rapidamente que o envelope ainda não atingiu a seção sustentar quando a chave é liberada? No começo eu pensei que o envelope devem completar suas seções de ataque e decaimento e depois ir direto para a seção de lançamento, mas isso não funcionou bem para um piano-tipo envelope. Em um envelope de piano, o nível de sustentar é zero e o tempo de decaimento é relativamente longo. Uma chave rapidamente pressionado e liberado ainda tinha uma longa decadência — como se isso não foram lançado em tudo!

Decidi que, para uma rápida pressione e solte, eu deixaria a seção de ataque completo, mas em seguida imediatamente saltar para a seção de lançamento. Isto significava que a taxa final de redução precisa ser calculado com base no nível atual. Isto explica porque é que há uma diferença em como o lançamento é tratado na estrutura para os parâmetros de envelope, mostrado aqui:

struct EnvelopeGeneratorParameters
{
  float baseLevel;
  float attackRate;   // in level/msec
  float peakLevel;
  float decayRate;    // in level/msec
  float sustainLevel;
  float releaseTime;  // in msec
};

Para o envelope de amplitude, baseLevel é definido como 0, peakLevel é definida como 1 e sustainLevel está em algum lugar entre esses valores. Para o envelope de filtro, os três níveis referem-se a um multiplicador aplicado para a frequência de corte do filtro: baseLevel é 1, e peakLevel é governada pelo controle deslizante rotulado "Envelope" e pode variar de 1 a 16. O multiplicador de freqüência de 16 corresponde a quatro oitavas.

Ambos AmplitudeEnvelopeEffect e FilterEnvelopeEffect compartilham a classe EnvelopeGenerator. Figura 4 mostra o arquivo de cabeçalho EnvelopeGenerator. Observe o método público para definir os parâmetros de envelope e dois métodos públicos chamados ataque e solte o gatilho o envelope para começar e terminar. Estes três métodos devem ser chamados nessa ordem. O código não escrito para lidar com um envelope, cujos parâmetros de mudam no meio do seu progresso.

Figura 4 o arquivo de cabeçalho EnvelopeGenerator.

class EnvelopeGenerator
{
private:
  enum class State
  {
    Dormant, Attack, Decay, Sustain, Release
  };
  EnvelopeGeneratorParameters params;
  float level;
  State state;
  bool isReleased;
  float releaseRate;
public:
  EnvelopeGenerator();
  void SetParameters(const EnvelopeGeneratorParameters params);
  void Attack();
  void Release();
  bool GetNextValue(float interval, float& value);
};

O atual valor calculado do gerador de envelope é obtido através de chamadas repetidas para GetNextValue. O iten de intervalo­ment em milissegundos, e o método calcula um novo valor baseado-se nesse intervalo, possivelmente Estados no processo de comutação. Quando o envelope foi concluída com a seção Released, GetNextValue retorna true para indicar que o envelope foi concluída, mas eu realmente não uso esse valor de retorno em outro lugar no programa.

Figura 5 mostra a implementação da classe EnvelopeGenerator. Perto do topo da GetNextValue método é o código para saltar directamente para o estado de libertação quando uma tecla é liberada, e o cálculo de uma taxa de liberação com base no nível atual e o tempo de liberação.

Figura 5 a implementação de EnvelopeGenerator

EnvelopeGenerator::EnvelopeGenerator() : state(State::Dormant)
{
  params.baseLevel = 0;
}
void EnvelopeGenerator::SetParameters(const EnvelopeGeneratorParameters params)
{
  this->params = params;
}
void EnvelopeGenerator::Attack()
{
  state = State::Attack;
  level = params.baseLevel;
  isReleased = false;
}
void EnvelopeGenerator::Release()
{
  isReleased = true;
}
bool EnvelopeGenerator::GetNextValue(float interval, float& value)
{
  bool completed = false;
  // If note is released, go directly to Release state,
  // except if still attacking
  if (isReleased &&
    (state == State::Decay || state == State::Sustain))
  {
    state = State::Release;
    releaseRate = (params.baseLevel - level) / params.releaseTime;
  }
  switch (state)
  {
  case State::Dormant:
    level = params.baseLevel;
    completed = true;
    break;
  case State::Attack:
    level += interval * params.attackRate;
    if ((params.attackRate > 0 && level >= params.peakLevel) ||
      (params.attackRate < 0 && level <= params.peakLevel))
    {
      level = params.peakLevel;
      state = State::Decay;
    }
    break;
  case State::Decay:
    level += interval * params.decayRate;
    if ((params.decayRate > 0 && level >= params.sustainLevel) ||
      (params.decayRate < 0 && level <= params.sustainLevel))
    {
      level = params.sustainLevel;
      state = State::Sustain;
    }
    break;
  case State::Sustain:
    break;
  case State::Release:
    level += interval * releaseRate;
    if ((releaseRate > 0 && level >= params.baseLevel) ||
      (releaseRate < 0 && level <= params.baseLevel))
    {
      level = params.baseLevel;
      state = State::Dormant;
      completed = true;
    }
  }
  value = level;
  return completed;
}

Um par de efeitos de áudio

Ambos os AmplitudeEnvelopeEffect e FilterEnvelopeEffect classes derivam de CXAPOParametersBase assim eles podem aceitar parâmetros, e ambas as classes também mantenham uma instância da classe EnvelopeGenerator para executar os cálculos de envelope. As estruturas de parâmetro para estes dois efeitos de áudio são chamadas AmplitudeEnvelopeParameters e FilterEnvelopeParameters.

A estrutura AmplitudeEnvelopeParameters é meramente uma estrutura de EnvelopeGeneratorParameters e um campo booleano keyPressed isso é verdadeiro quando a chave associada com esta voz é pressionado e falso quando for lançado. (O filtro­EnvelopeParameters estrutura é apenas um pouco mais complexa, porque ele precisa incorporar uma frequência de corte do filtro de nível básico e configuração Q.) Ambas as classes de efeitos mantêm suas próprias keyPressed membros de dados que podem ser comparados com o valor de parâmetros para determinar quando o envelope é atacar ou liberação do Estado deve ser disparada.

Você pode ver como isso funciona Figura 6, que mostra o código para substituir o processo em AmplitudeEnvelopeEffect. Se o efeito é ativado e o keyPressed local valor é false, mas o valor de keyPressed nos parâmetros de efeito é verdadeiro, então o efeito faz chamadas para os métodos SetParameters e ataque da instância EnvelopeGenerator. Se o contrário for o caso — o valor local keyPressed é verdade mas nos parâmetros é falsa — em seguida, o efeito chama o método de liberação.

Figura 6 o processo de substituir em AmplitudeEnvelopeEffect

void AmplitudeEnvelopeEffect::Process(UINT32 inpParamCount,
    const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParam,
    UINT32 outParamCount,
    XAPO_PROCESS_BUFFER_PARAMETERS *pOutParam,
    BOOL isEnabled)
{
  // Get effect parameters
  AmplitudeEnvelopeParameters * pParams =
    reinterpret_cast<AmplitudeEnvelopeParameters *>
      (CXAPOParametersBase::BeginProcess());
  // Get buffer pointers and other information
  const float * pSrc = static_cast<float const*>(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:
    if (!isEnabled)
    {
      for (int frame = 0; frame < frameCount; frame++)
      {
        for (int channel = 0; channel < numChannels; channel++)
        {
          int index = numChannels * frame + channel;
          pDst[index] = pSrc[index];
        }
      }
    }
    else
    {
      // Key being pressed
      if (!this->keyPressed && pParams->keyPressed)
      {
        this->keyPressed = true;
        this->envelopeGenerator.SetParameters(pParams->envelopeParams);
        this->envelopeGenerator.Attack();
      }
      // Key being released
      else if (this->keyPressed && !pParams->keyPressed)
      {
        this->keyPressed = false;
        this->envelopeGenerator.Release();
      }
      // Calculate interval in msec
      float interval = 1000.0f / waveFormat.
nSamplesPerSec;
      for (int frame = 0; frame < frameCount; frame++)
      {
        float volume;
        envelopeGenerator.GetNextValue(interval, volume);
        for (int channel = 0; channel < numChannels; channel++)
        {
          int index = numChannels * frame + channel;
          pDst[index] = volume * pSrc[index];
        }
      }
    }
    break;
  case XAPO_BUFFER_SILENT:
    break;
  }
  // Set output parameters
  pOutParam[0].ValidFrameCount = pInpParam[0].ValidFrameCount;
  pOutParam[0].BufferFlags = pInpParam[0].BufferFlags;
  CXAPOParametersBase::EndProcess();
}

O efeito poderia chamar o método de GetNextValue de EnvelopeGenerator para cada chamada de processo (em cujo caso o argumento interval indicaria 10 milissegundos) ou para cada amostra (caso em que o intervalo é mais como 21 microssegundos). Embora a primeira abordagem deve ser adequada, me decidi pelo segundo para transições teoricamente mais suaves.

O valor de ponto flutuante volume retornado do GetNextValue chamada varia de 0 (quando uma nota é primeira começando ou terminando) 1 para o culminar do ataque. O efeito simplesmente multiplica as amostras de ponto flutuante por esse número.

Agora começa a diversão

Passei tanto tempo a codificação do analógico­programa de Synth que eu não tive muito tempo para brincar com ele. Poderia muito bem ser que alguns dos controles e parâmetros precisam de algum ajuste fino, ou talvez um pouco mais grosseira tuning! Em particular, longa decadência e lançamento vezes no volume não som muito bem, e eles sugerem que as alterações de envelope para amplitudes devem ser logarítmica, em vez de linear.

Eu também estou intrigado com o uso de entrada por toque com o teclado no ecrã. As chaves em um piano de verdade são sensíveis à velocidade com a qual eles são atingidos, e teclados sintetizador tentou emular essa mesma sensação. A maioria das telas sensíveis ao toque, no entanto, não consegue detectar a velocidade de toque ou pressão. Mas podem ser feitos sensíveis aos movimentos do dedo ligeiro na tela, que está além da capacidade de um teclado real. Podem na tela teclados ser feitos mais ágil desta forma? Há apenas uma maneira de descobrir!

Charles Petzold é colaborador da MSDN Magazine há muito tempo e é o autor de “Programming Windows, 6th edition” (O’Reilly Media, 2012), um livro sobre a criação de aplicativos para o Windows 8. O site dele é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: James McNellis (Microsoft)
James McNellis é um aficionado de C++ e um desenvolvedor de software na equipe do Visual C++ da Microsoft, onde ele onde ele constrói bibliotecas C++ e mantém as bibliotecas C Runtime (CRT).  Ele tweets no @JamesMcNellise pode ser encontrada em outro lugar on-line via http://jamesmcnellis.com/.