Toque e ouça

Instrumentos musicais para o Windows Phone

Charles Petzold

Baixar o código de exemplo

Charles PetzoldTodo Windows Phone tem um alto-falante interno e um fone de ouvido, e seria uma pena se o uso deles se restringisse a fazer chamadas telefônicas. Felizmente, os aplicativos do Windows Phone também podem usar os recursos de áudio do telefone para reproduzir músicas ou outros sons. Como demonstrei em edições recentes desta coluna, um aplicativo do Windows Phone pode reproduzir arquivos MP3 ou WMA armazenados na biblioteca de músicas do usuário ou arquivos baixados pela Internet.

Um aplicativo do Windows Phone também pode gerar formas de onda de áudio dinamicamente, uma técnica chamada “streaming de áudio”. Essa atividade usa uma imensa quantidade de dados: para som com qualidade de CD, é preciso gerar amostras de 16 bits a uma velocidade de 44.100 amostras por segundo para os canais esquerdo e direito ou colossais 176.400 bytes por segundo!

Mas o streaming de áudio é uma técnica avançada. Se você combiná-la a multitoque, poderá transformar o telefone em um instrumento musical eletrônico, e o que pode ser mais divertido do que isso?

Concebendo um teremim

Um dos primeiros instrumentos musicais eletrônicos foi criado pelo inventor russo Léon Theremin nos anos 1920. O músico de um teremim na verdade não toca no instrumento. Em vez disso, suas mãos se movimentam em relação a duas antenas, que controlam o volume e o timbre do som separadamente. O resultado é um lamento tremido assombrado que emite as notas uma a uma — visto em filmes como “Quando fala o coração” e “O dia em que a Terra parou” e no episódio 12 da quarta temporada do seriado “The Big Bang Theory”. (Ao contrário da crença popular, não foi usado um teremim no tema de “Jornada nas estrelas”.)

Um Windows Phone pode se transformar em um teremim portátil? Esse era o meu objetivo.

O teremim clássico produz som através de uma técnica heteródina pela qual duas formas de onda de alta frequência são combinadas para produzir um tom diferente no intervalo de áudio. Mas essa técnica não é prática quando formas de onda são geradas em um software de computador. É muito mais sensato gerar a forma de onda de áudio diretamente.

Depois de brincar um pouco com a ideia de usar a orientação do telefone para controlar o som, ou de o programa ver e interpretar movimentos manuais através da câmera do telefone à la Kinect, escolhi uma abordagem muito mais prosaica: um dedo na tela do telefone é um ponto de coordenada bidimensional, que permite a um programa usar um eixo para frequência e o outro para amplitude.

Para fazer isso com inteligência, é preciso um certo conhecimento sobre a nossa percepção de sons musical.

Pixels, timbres e amplitudes

Graças ao trabalho pioneiro de Ernst Weber e Gustav Fechner no século 19, sabemos que a percepção humana é logarítmica e não linear. Alterações lineares incrementais na magnitude de estímulo não são percebidas como iguais. Ao contrário, o que percebemos como iguais são alterações proporcionais à magnitude, muitas vezes convenientemente expressas como aumentos ou diminuições fracionárias. (Esse fenômeno vai além dos nossos órgãos sensoriais. Por exemplo, percebemos que a diferença entre $1 e $2 é bem maior do que a diferença entre $100 e $101.)

Os seres humanos são sensíveis a frequências de áudio entre aproximadamente 20Hz e 20.000Hz, mas a nossa percepção de frequência não é linear. Em muitas culturas, o timbre musical é estruturado em torno da oitava, que é uma duplicação de frequência. Quando você canta “Somewhere Over the Rainbow”, as duas sílabas da primeira palavra são uma oitava à parte, independentemente de a transição ser de 100Hz para 200Hz ou de 1.000Hz para 2.000Hz. O alcance da audição humana é, portanto, de cerca de 10 oitavas.

A oitava tem esse nome porque, na música ocidental, uma oitava é formada por oito notas designadas por letras de uma escala onde a última nota é uma oitava mais alta do que a primeira: A, B, C, D, E, F, G, A (a escala menor) ou C, D, E, F, G, A, B, C (a escala maior).

Devido à forma como são derivadas, essas notas não são igualmente distantes umas das outras em termos de percepção. Uma escala em que todas as notas são igualmente distantes exige cinco notas a mais, totalizando 12 (não contando a primeira nota duas vezes): C, C#, D, D#, E, F, F#, G, G#, A, A# e B. Cada um desses intervalos é conhecido como semitom e, se forem igualmente espaçados (como são no ajuste de afinação igual comum), cada nota terá uma frequência que é a décima segunda raiz de duas (ou cerca de 1,059) vezes a frequência da nota abaixo dela.

O semitom pode ser dividido em 100 centos. Existem 1.200 centos até a oitava. O intervalo multiplicativo entre centos é a milésima ducentésima raiz de dois, ou 1,000578. A sensibilidade humana a alterações na frequência varia muito, é claro, mas em geral fica em torno de cinco centos.

Essas informações preliminares sobre a física e a matemática da música são necessárias porque o programa Theremin precisa converter a localização de pixel de um dedo em uma frequência. Essa conversão deve ser feita para que cada oitava corresponda a um número igual de pixels. Se definirmos que o Theremin deve ter um alcance de quatro oitavas correspondente aos 800 pixels da tela do Windows Phone no modo paisagem, isso são 200 pixels por oitava, ou seis centos por pixel, o que corresponde bem aos limites da percepção humana.

A amplitude de uma forma de onda determina como percebemos o volume, e isso também é logarítmico. Um decibel é definido como 10 vezes o logaritmo na base 10 da razão de dois níveis de potência. Como a potência de uma forma de onda é o quadrado da amplitude, a diferença em decibéis entre duas amplitudes é a seguinte:

O áudio de CD usa amostras de 16 bits, o que permite que a razão entre as amplitudes máxima e mínima seja de 65.536. Pegue o algoritmo na base 10 de 65.536, multiplique por 20 e você terá um alcance de 96 decibéis.

Um decibel corresponde a um aumento de cerca de 12% em amplitude. A percepção humana em relação a alterações de amplitude é bem menor do que em relação a frequência. São necessários alguns decibéis para que as pessoas percebam um aumento de volume, então isso pode ser facilmente acomodado na dimensão de 480 pixels da tela do Windows Phone.

Tornando real

O código para download deste artigo é uma solução do Visual Studio chamada MusicalInstruments. O projeto Petzold.MusicSynthesis é uma DLL que contém principalmente os arquivos mencionados na edição do mês passado desta coluna (msdn.microsoft.com/magazine/hh852599). O projeto do aplicativo Theremin consiste em uma única página paisagem.

Que tipo de forma de onda um teremim deve gerar? Em teoria, trata-se de uma onda senoidal, mas na prática é uma onda senoidal um pouco distorcida e, se você tentar procurar essa pergunta na Internet, não encontrará muito consenso. Para a minha versão, fiquei com uma onda senoidal contínua e me pareceu razoável.

Conforme ilustrado na Figura 1, o arquivo MainPage.xaml.cs define diversos valores de constante e calcula dois inteiros que determinam como os pixels da tela correspondem a notas.

Figura 1 Cálculo de amplitude e frequência para o Theremin

public partial class MainPage : PhoneApplicationPage
{
  static readonly Pitch MIN_PITCH = new Pitch(Note.C, 3);
  static readonly Pitch MAX_PITCH = new Pitch(Note.C, 7);
  static readonly double MIN_FREQ = MIN_PITCH.Frequency;
  static readonly double MAX_FREQ = MAX_PITCH.Frequency;
  static readonly double MIN_FREQ_LOG2 = Math.Log(MIN_FREQ) / Math.Log(2);
  static readonly double MAX_FREQ_LOG2 = Math.Log(MAX_FREQ) / Math.Log(2);
  ...
  double xStart;      // The X coordinate corresponding to MIN_PITCH
  int xDelta;         // The number of pixels per semitone
  void OnLoaded(object sender, EventArgs args)
  {
    int count = MAX_PITCH.MidiNumber - MIN_PITCH.MidiNumber;
    xDelta = (int)((ContentPanel.ActualWidth - 4) / count);
    xStart = (int)((ContentPanel.ActualWidth - count * xDelta) / 2);
    ...
  }
  ...
  double CalculateAmplitude(double y)
  {
    return Math.Min(1, Math.Pow(10, -4 * (1 - y / ContentPanel.ActualHeight)));
  }
  double CalculateFrequency(double x)
  {
    return Math.Pow(2, MIN_FREQ_LOG2 + (x - xStart) / xDelta / 12);
  }
  ...
}

O alcance é do C abaixo do C intermediário (uma frequência de aproximadamente 130,8Hz) até as três oitavas de C acima do C intermediário, cerca de 2.093Hz. Dois métodos calculam uma frequência e uma amplitude relativa (variando de 0 a 1) com base nas coordenadas de um ponto de toque obtido do evento Touch.FrameReported. 

Se você usar apenas esses valores para controlar um oscilador de onda senoidal, o som não será parecido com o de um teremim. Conforme você movimenta o dedo na tela, o programa não obtém um evento para cada pixel encontrado. Em vez de uma transição de frequência suave, você ouvirá intervalos muito descontínuos. Para resolver esse problema, criei uma classe de oscilador especial mostrada na Figura 2. Esse oscilador herda uma propriedade Frequency, mas define outras três propriedades: Amplitude, DestinationAmplitude e DestinationFrequency. Usando fatores multiplicativos, o próprio oscilador cuida da transição. Na verdade, o código não consegue prever a rapidez da movimentação de um dedo, mas na maioria dos casos parece funcionar bem.

Figura 2 A classe ThereminOscillator

public class ThereminOscillator : Oscillator
{
  readonly double ampStep;
  readonly double freqStep;
  public const double MIN_AMPLITUDE = 0.0001;
  public ThereminOscillator(int sampleRate)
    : base(sampleRate)
  {
    ampStep = 1 + 0.12 * 1000 / sampleRate;     // ~1 db per msec
    freqStep = 1 + 0.005 * 1000 / sampleRate;   // ~10 cents per msec
  }
  public double Amplitude { set; get; }
  public double DestinationAmplitude { get; set; }
  public double DestinationFrequency { set; get; }
  public override short GetNextSample(double angle)
  {
    this.Frequency *= this.Frequency < this.DestinationFrequency ?
                                     freqStep : 1 / freqStep;
    this.Amplitude *= this.Amplitude < this.DestinationAmplitude ?
                                     ampStep : 1 / ampStep;
    this.Amplitude = Math.Max(MIN_AMPLITUDE, Math.Min(1, this.Amplitude));
    return (short)(short.MaxValue * this.Amplitude * Math.Sin(angle));
  }
}

A Figura 3 mostra o manipulador do evento Touch.FrameReported na classe MainPage. Quando um dedo toca na tela pela primeira vez, Amplitude é definida com um valor mínimo para que o volume do som aumente. Quando você tira o dedo, o som diminui gradualmente.

Figura 3 O manipulador Touch.FrameReported no Theremin

void OnTouchFrameReported(object sender, TouchFrameEventArgs args)
{
  TouchPointCollection touchPoints = args.GetTouchPoints(ContentPanel);
  foreach (TouchPoint touchPoint in touchPoints)
  {
    Point pt = touchPoint.Position;
    int id = touchPoint.TouchDevice.Id;
    switch (touchPoint.Action)
    {
      case TouchAction.Down:
        oscillator.Amplitude = ThereminOscillator.MIN_AMPLITUDE;
        oscillator.DestinationAmplitude = CalculateAmplitude(pt.Y);
        oscillator.Frequency = CalculateFrequency(pt.X);
        oscillator.DestinationFrequency = oscillator.Frequency;
        HighlightLines(pt.X, true);
        touchID = id;
        break;
      case TouchAction.Move:
        if (id == touchID)
        {
           oscillator.DestinationFrequency = CalculateFrequency(pt.X);
           oscillator.DestinationAmplitude = CalculateAmplitude(pt.Y);
           HighlightLines(pt.X, true);
        }
        break;
      case TouchAction.Up:
        if (id == touchID)
        {
          oscillator.DestinationAmplitude = 0;
          touchID = Int32.MinValue;
          // Remove highlighting
          HighlightLines(0, false);
        }
        break;
      }
    }
}

Como podemos ver no código, o programa Theremin gera apenas um tom e ignora vários dedos.

Embora a frequência do teremim varie continuamente, a tela exibe linhas para indicar notas distintas. Essas linhas têm a cor vermelho para C e azul para F (as cores utilizadas para cordas da harpa), branco para bequadros e cinza para acidentes (os sustenidos). Depois de brincar um pouco com o programa, cheguei à conclusão de que precisava de um feedback visual que indicasse em qual nota o dedo estava posicionado e alarguei as linhas com base na distância delas a partir do ponto de toque. A Figura 4mostra a tela quando o dedo está entre C e C#, mas mais próximo de C.

The Theremin Display
Figura 4 A tela do Theremin

Latência e distorção

Um grande problema da síntese musical baseada em software é a latência, ou seja, o atraso entre a entrada do usuário e a subsequente alteração no som. Isto é inevitável: o streaming de áudio no Silverlight exige que um aplicativo derive de MediaStreamSource e ignore o método GetSampleAsync, que fornece dados de áudio sob demanda através de um objeto Memory­Stream. Internamente, esses dados de áudio são mantidos em um buffer. A existência desse buffer ajuda a garantir que o som seja reproduzido sem nenhuma lacuna desconcertante, mas certamente a reprodução do buffer sempre deixará para trás o preenchimento do buffer.

Felizmente, MediaStreamSource define uma propriedade chamada AudioBufferLength que indica o tamanho do buffer em milissegundos de som. (Essa propriedade é protegida e só pode ser configurada no derivativo MediaStreamSource antes da abertura da mídia.) O valor padrão é 1.000 (ou 1 segundo), mas é possível defini-lo como 15. Uma configuração mais baixa aumenta a interação entre o SO e o derivativo MediaStreamSource e pode gerar lacunas no som. No entanto, descobri que a configuração mínima de 15 parece satisfatória.

Outro problema em potencial é simplesmente não poder construir os dados. O seu programa precisa gerar dezenas ou centenas de milhares de bytes por segundo e, se ele não puder fazer isso com eficiência, o som começará a dispersar e você ouvirá muitos estalidos.

Existem algumas formas de corrigir isso: você pode tornar o pipeline de geração de áudio mais eficiente (abordarei esse assunto resumidamente)ou reduzir a taxa de amostragem. Descobri que a taxa de amostragem de CD de 44.100 era muito alta para os meus programas e a reduzi para 22.050. Talvez também seja necessário reduzi-la ainda mais, para 11.025. Sempre é bom testar os programas de áudio em diferentes dispositivos Windows Phone. Em um produto comercial, convém dar ao usuário a opção de reduzir a taxa de amostragem.

Vários osciladores

O componente Mixer da biblioteca de sintetizadores é responsável por reunir várias entradas em canais esquerdo e direito compostos. Essa é uma tarefa bastante simples, mas lembre-se de que cada entrada é uma forma de onda com amplitude de 16 bits e que a saída também é uma forma de onda com amplitude de 16 bits, por isso as entradas devem ser aten­uadas de acordo com a sua quantidade. Por exemplo, se o componente Mixer tiver 10 entradas, cada entrada deverá ser atenuada para um décimo de seu valor original.

Isso tem uma implicação profunda: não é possível adicionar ou remover entradas do Mixer enquanto a música está tocando sem aumentar ou diminuir o volume das entradas restantes. Se você deseja um programa com potencial para reproduzir 25 sons diferentes ao mesmo tempo, precisará de 25 entradas constantes do Mixer.

Esse é o caso do aplicativo Harp na solução MusicalInstruments. Vislumbrei um instrumentos com cordas que eu poderia tanger com a ponta dos dedos, mas que também poderia dedilhar para obter o som comum de glissandos de harpa.

Como podemos ver na Figura 5, visualmente ele é muito parecido com o teremim, mas com apenas duas oitavas em vez de quatro. As cordas para os acidentes (sustenidos) estão posicionadas na parte superior, enquanto os bequadros estão na parte inferior, o que de algum modo imita o tipo de harpa conhecido como harpa cross-strung. É possível executar um glissando pentatônico (no alto), um glissando cromático (no meio) ou um glissando diatônico (embaixo).

The Harp Program
Figura 5 O programa Harp

Para os sons propriamente ditos, usei 25 instâncias de uma classe SawtoothOscillator, que gera uma forma de onda serreada simples que se aproxima grosseiramente do som de uma corda. Também foi necessário criar um gerador de envelope rudimentar. Na vida real, os sons musicais não começam e param instantaneamente. Demora um pouco até o som começar a fluir e, então, ele pode ir diminuindo (como no caso de um piano ou uma harpa) ou diminuir gradativamente depois que o músico para de tocá-lo. Um gerador de envelope controla essas alterações. Não precisei de nada tão sofisticado quanto um envelope ADSR (Attack-Decay-Sustain-Release) completo, por isso criei uma classe AttackDecayEnvelope mais simples. (Na vida real, o timbre de um som, controlado por seus componentes harmônicos, também muda durante um único tom, por isso ele também deve ser controlado por um gerador de envelope.)

Para feedback visual, conclui que gostaria que as cordas vibrassem. Cada corda é, na verdade, um segmento de Bézier quadrático, com o ponto de controle central colinear com os dois pontos de extremidade. Aplicando um PointAnimation repetitivo ao ponto de controle, eu poderia fazer as cordas vibrar.

Na prática, foi um desastre. As vibrações pareciam ótimas, mas o som se transformou em estalidos horrorosos. Mudei para algo um pouco menos sério: usei um DispatcherTimer e desloquei os pontos manualmente muito mais lentamente do que uma animação real.

Depois de brincar um pouco com o programa Harp, fiquei insatisfeito com o gesto de movimento necessário para tanger as cordas, por isso adicionei um código para disparar o som com um simples toque. Nesse momento, eu deveria ter alterado o nome do programa de Harp para HammeredDulcimer, mas deixei para lá.

Evitando o ponto flutuante

No dispositivo Windows Phone que eu estava usando para a maior parte do trabalho de desenvolvimento, o Harp funcionou bem. Em outro Windows Phone, ele ficou extremamente instável, indicando que os buffers não poderiam ser preenchidos com rapidez suficiente. Essa análise foi confirmada com a redução da taxa de amostragem à metade. Os estalidos cessaram com uma taxa de amostragem de 11.025Hz, mas eu não estava pronto para sacrificar a qualidade do som.

Em vez disso, comecei a analisar o pipeline que fornecia essas milhares de amostras por segundo. Essas classes, Mixer, MixerInput, SawtoothOscillator e AttackDecayEnvelope, tinham algo em comum: todas elas usavam aritmética de ponto flutuante de alguma forma para calcular essas amostras. Alternar para cálculos de inteiro poderia ajudar a acelerar esse pipeline o suficiente para fazer uma diferença?

Reescrevi minha classe AttackDecayEnvelope para usar a aritmética de inteiro e fiz o mesmo com SawtoothOscillator, mostrada na Figura 6. Essas alterações melhoraram o desempenho consideravelmente.

Figura 6 A versão de inteiro da classe SawtoothOscillator

public class SawtoothOscillator : IMonoSampleProvider
{
  int sampleRate;
  uint angle;
  uint angleIncrement;
  public SawtoothOscillator(int sampleRate)
  {
    this.sampleRate = sampleRate;
  }
  public double Frequency
  {
    set
    {
      angleIncrement = (uint)(UInt32.MaxValue * value / sampleRate);
    }
    get
    {
      return (double)angleIncrement * sampleRate / UInt32.MaxValue;
    }
  }
  public short GetNextSample()
  {
    angle += angleIncrement;
    return (short)((angle >> 16) + short.MinValue);
  }
}

Nos osciladores que usam ponto flutuante, as variáveis angle e angle­Increment são do tipo double, em que angle varia de 0 a 2π e angleIncrement é calculada da seguinte maneira:

Para cada amostra, angle é aumentada por angleIncrement.

Não eliminei totalmente o ponto flutuante de SawtoothOscillator. A propriedade pública Frequency ainda é definida como double, mas só é usada quando a frequência do oscilador está definida. Tanto angle quanto angleIncrement são inteiros de 32 bits sem sinal. Os valores de 32 bits completos são usados quando angleIncrement aumenta o valor de angle, mas somente os primeiros 16 bits são utilizados como valor para calcular uma forma de onda.

Mesmo com essas alterações, o programa ainda não funciona bem no que agora considero como sendo o meu “telefone lento” em comparação com o meu “telefone rápido”. Deslizar um dedo pela tela inteira ainda gera estalidos.

Mas o que é válido para qualquer instrumento musical também é válido para instrumentos musicais eletrônicos: você precisa se familiarizar com o instrumento e conhecer não somente a capacidade, mas também as limitações.

Charles Petzold é colaborador da MSDN Magazine há muito tempo. O endereço do seu site é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo:  Mark Hopkins