Fator DirectX

Transmitindo e manipulando arquivos de áudio no Windows 8

Charles Petzold

Baixar o código de exemplo

Charles PetzoldAtualmente, muitos usuários do Windows têm uma biblioteca de música em seus discos rígidos contendo talvez milhares ou dezenas de milhares de arquivos MP3 e WMA. Para tocar essa música no PC, geralmente tais usuários executam o Windows Media Player ou o aplicativo de música do Windows 8. Mas para programadores, é bom saber que podemos escrever nossos próprios programas para executar esses arquivos. O Windows 8 fornece interfaces de programação para acessar a Biblioteca de Música, obter informações sobre os arquivos de música individuais (como artista, título e duração) e executar esses arquivos usando o MediaElement.

O MediaElement é a abordagem fácil, e certamente há alternativas que fazem o trabalho mais difícil, mas também adicionam muita versatilidade. Com dois componentes do DirectX, Media Foundation e XAudio2, é possível para um aplicativo envolver-se muito mais nesse processo. Você pode carregar partes de dados de áudio descompactados de arquivos de música e analisar esses dados e manipulá-los de alguma forma antes (ou em vez) de tocar a música. Você já imaginou como um Estudo de Chopin soaria quando tocado de trás pra frente ou pela metade da velocidade? Bem, eu também não, mas um dos programas fornecidos com este artigo permitirá descobrir.

Selecionadores de acesso em massa

Com certeza, a maneira mais fácil para um programa Windows 8 acessar a Biblioteca de Música é por meio do FileOpenPicker, que pode ser inicializado em um programa C++ para carregar arquivos de áudio como a seguir:

 

FileOpenPicker^ fileOpenPicker = ref new FileOpenPicker();
fileOpenPicker->SuggestedStartLocation = 
  PickerLocationId::MusicLibrary;
fileOpenPicker->FileTypeFilter->Append(".wma");
fileOpenPicker->FileTypeFilter->Append(".mp3");
fileOpenPicker->FileTypeFilter->Append(".wav");

Chame PickSingleFileAsync para exibir o FileOpenPicker e deixe o usuário selecionar um arquivo.

Para uma exploração de forma livre das pastas e arquivos, também é possível para o arquivo de manifesto do aplicativo indicar que ele deseja acesso mais abrangente à Biblioteca de Música. O programa pode então usar as classes no namespace Windows::Storage::BulkAccess para enumerar as pastas e arquivos de música por sua conta.

Independentemente da abordagem adotada, cada arquivo é representado por um objeto StorageFile. Desse objeto você pode obter uma miniatura, que é uma imagem da capa do álbum de música (se existir). Da propriedade Properties de StorageFile, você pode obter um objeto MusicProperties, que fornece o artista, o álbum, o nome da trilha, a duração e outras informações padrão associadas ao arquivo de música.

Ao chamar OpenAsync nesse StorageFile, você também pode abri-lo para leitura e obter um objeto IRandomAccessStream, e até mesmo ler o arquivo inteiro na memória. Se for um arquivo WAV, considere analisar o arquivo, extrair os dados da forma de onda e tocar o som com o XAudio2, como descrevi em edições recentes desta coluna.

Mas se for um arquivo MP3 ou WMA, isso não é tão fácil. Você precisará descompactar os dados de áudio, e esse é um trabalho que talvez você não queira assumir fazer. Felizmente, as APIs do Media Foundation incluem recursos para descompactar arquivos MP3 e WMA e colocar os dados em um formato que pode ser passado diretamente ao XAudio2 para execução.

Uma outra abordagem para obter acesso a dados de áudio descompactados é através de um efeito de áudio que está conectado a um MediaElement. Espero demonstrar essa técnica em um artigo posterior.

Transmitindo Media Foundation

Para usar as funções e interfaces do Media Foundation que discutirei aqui, você precisará vincular seu programa Windows 8 às bibliotecas de importação mfplat.lib e mfreadwrite.lib, e precisará de instruções #include para mfapi.h, mfidl.h e mfreadwrite.h em seu arquivo pch.h. (Ainda, certifique-se de incluir initguid.h antes de mfapi.h ou obterá erros de vinculação que poderão deixá-lo desnorteado por muitas horas não produtivas.) Se também estiver usando XAudio2 para executar os arquivos (como o farei aqui), você precisará da biblioteca de importação xaudio2.lib e do arquivo de cabeçalho xaudio2.h.

Entre o código que pode ser baixado para essa coluna está um projeto do Windows 8 chamado StreamMusicFile que demonstra exatamente o código mínimo necessário para carregar um arquivo da Biblioteca de Música do PC, descompactá-lo com o Media Foundation e executá-lo com o XAudio2. Um botão chama o FileOpenPicker, e depois de selecionar um arquivo, o programa exibe algumas informações padrão (como mostrado na Figura 1) e imediatamente começa a executar o arquivo. Por padrão, o Controle deslizante de volume na parte inferior está definido como 0. Você precisará aumentá-lo para ouvir alguma coisa. Não há nenhuma maneira de pausar ou parar o arquivo exceto encerrando o programa ou trazendo outro programa para o primeiro plano.

The StreamMusicFile Program Playing a Music File
Figura 1 O programa StreamMusicFile executando um arquivo de música

De fato, o programa não para de executar um arquivo de música mesmo se você clicar no botão e carregar um segundo arquivo. Em vez disso, você descobrirá que ambos os arquivos são executados ao mesmo tempo, mas talvez não em qualquer tipo de sincronização coerente. Portanto, isso é algo que esse programa pode fazer que o aplicativo de música do Windows 8 e o Media Player não podem: executar diversos arquivos de música simultaneamente.

O método mostrado na Figura 2 mostra como o programa usa um IRandomAccessStream de um StorageFile para criar um objeto IMFSourceReader capaz de ler um arquivo de áudio e entregar partes de dados de áudio descompactados.

Figura 2 Criando e inicializando um IMFSourceReader

 

ComPtr<IMFSourceReader> MainPage::CreateSourceReader(IRandomAccessStream^ randomAccessStream)
{
  // Start up Media Foundation
  HRESULT hresult = MFStartup(MF_VERSION);
  // Create a IMFByteStream to wrap the IRandomAccessStream
  ComPtr<IMFByteStream> mfByteStream;
  hresult = MFCreateMFByteStreamOnStreamEx((IUnknown *)randomAccessStream,
                                            &mfByteStream);
  // Create an attribute for low latency operation
  ComPtr<IMFAttributes> mfAttributes;
  hresult = MFCreateAttributes(&mfAttributes, 1);
  hresult = mfAttributes->SetUINT32(MF_LOW_LATENCY, TRUE);
  // Create the IMFSourceReader
  ComPtr<IMFSourceReader> mfSourceReader;
  hresult = MFCreateSourceReaderFromByteStream(mfByteStream.Get(),
                                               mfAttributes.Get(),
                                               &mfSourceReader);
  // Create an IMFMediaType for setting the desired format
  ComPtr<IMFMediaType> mfMediaType;
  hresult = MFCreateMediaType(&mfMediaType);
  hresult = mfMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
  hresult = mfMediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float);
  // Set the media type in the source reader
  hresult = mfSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM,
                                          0, mfMediaType.Get());
  return mfSourceReader;
}

Para maior clareza, a Figura 2 exclui todo o código que manipula valores de retorno HRESULT errôneos. O código real gera exceções do tipo COMException, mas o programa não captura essas exceções como um aplicativo real o faria.

Resumindo, esse método usa o IRandomAccessStream para criar um objeto IMFByteStream que encapsula o fluxo de entrada e, em seguida, usa isso para criar um IMFSourceReader, que pode executar a descompactação real.

Observe o uso de um objeto IMFAttributes para especificar uma operação de baixa latência. Isso não é estritamente exigido e você pode definir o segundo argumento da função MFCreateSourceReaderFromByteStream como nullptr. No entanto, como o arquivo está sendo lido e executado, o disco rígido está sendo acessado e você não quer que estas operações de disco criem intervalos audíveis na reprodução. Se, realmente, você estiver preocupado com esse problema, considere ler o arquivo inteiro em um objeto InMemoryRandomAccessStream e usar isso para criar o IMFByteStream.

Quando um programa usa o Media Foundation para descompactar um arquivo de áudio, o programa não tem controle sobre a taxa de amostragem dos dados descompactados que ele recebe do arquivo, ou o número de canais. Isso é governado pelo arquivo. No entanto, o programa pode especificar que as amostras estejam em um de dois formatos diferentes: inteiros de 16 bits (usados para áudio de CD) ou valores de ponto flutuante de 32 bits (o tipo flutuante C). Internamente, o XAudio2 usa amostras de ponto flutuante de 32 bits, portanto, menos conversões internas são necessárias se amostras de ponto flutuante de 32 bits são passadas para o XAudio2 para execução do arquivo. Decidi seguir esse caminho neste programa. Dessa maneira, o método na Figura 2 especifica o formato dos dados de áudio desejados com os dois identificadores MFMediaType_Audio e MFAudioFormat_Float. Se dados descompactados forem exigidos, a única alternativa desse segundo identificador é MFAudioFormat_PCM para amostras de inteiros de 16 bits.

Neste ponto, temos um objeto do tipo IMFSourceReader pronto para ler e descompactar partes de um arquivo de áudio.

Executando o arquivo

Originalmente, eu queria ter todo o código deste primeiro programa na classe MainPage, mas também queria usar uma função de retorno de chamada do XAudio2. Isso é um problema porque (como descobri) um tipo do Tempo de Execução do Windows como MainPage não pode implementar uma interface que não seja do Tempo de Execução do Windows como IXAudio2VoiceCallback, portanto, precisava de uma segunda classe, que chamei de AudioFilePlayer.

Depois de obter um objeto IMFSourceReader do método mostrado na Figura 2, MainPage cria um novo objeto AudioFilePlayer, também passando para ele um objeto IXAudio2 criado no construtor MainPage:

new AudioFilePlayer(pXAudio2, mfSourceReader);

Desse ponto, o objeto AudioFilePlayer está completamente sozinho e é bastante autossuficiente. É como o programa pode executar diversos arquivos simultaneamente.

Para executar o arquivo de música, AudioFilePlayer precisa criar um objeto IXAudio2­SourceVoice. Isso requer uma estrutura WAVEFORMATEX indicando o formato dos dados de áudio a serem passados para a voz fonte. Isso deve ser consistente com os dados de áudio sendo entregues pelo objeto IMFSourceReader. Talvez você possa imaginar os parâmetros corretos (como dois canais e uma taxa de amostragem de 44.100 Hz), e se você se enganar com a taxa de amostragem, o XAudio2 pode executar conversões de taxa de amostragem internamente. Ainda, é melhor obter um estrutura WAVEFORMATEX do IMFSourceReader e usá-la, como mostrado no construtor AudioFilePlayer na Figura 3.

Figura 3 O construtor AudioFilePlayer em StreamMusicFile

AudioFilePlayer::AudioFilePlayer(ComPtr<IXAudio2> pXAudio2,
                                 ComPtr<IMFSourceReader> mfSourceReader)
{
  this->mfSourceReader = mfSourceReader;
  // Get the Media Foundation media type
  ComPtr<IMFMediaType> mfMediaType;
  HRESULT hresult = mfSourceReader->GetCurrentMediaType(MF_SOURCE_READER_
                                                        FIRST_AUDIO_STREAM,
                                                        &mfMediaType);
  // Create a WAVEFORMATEX from the media type
  WAVEFORMATEX* pWaveFormat;
  unsigned int waveFormatLength;
  hresult = MFCreateWaveFormatExFromMFMediaType(mfMediaType.Get(),
                                                &pWaveFormat,
                                                &waveFormatLength);
  // Create the XAudio2 source voice
  hresult = pXAudio2->CreateSourceVoice(&pSourceVoice, pWaveFormat,
                                        XAUDIO2_VOICE_NOPITCH, 1.0f, this);
  // Free the memory allocated by function
  CoTaskMemFree(pWaveFormat);
  // Submit two buffers
  SubmitBuffer();
  SubmitBuffer();
  // Start the voice playing
  pSourceVoice->Start();
  endOfFile = false;
}

Obter essa estrutura WAVEFORMATEX é um incômodo que envolve um bloco de memória que deve então ser explicitamente liberado, mas na conclusão do construtor AudioFilePlayer, o arquivo está pronto para ser executado.

Para manter o volume de memória de tal programa no mínimo, o arquivo deve ser lido e executado em pequenas partes. O Media Foundation e o XAudio2 são muito úteis nessa abordagem. Cada chamada para o método ReadSample do objeto IMFSourceReader obtém acesso ao próximo bloco de dados descompactados até que o arquivo seja totalmente lido. Para uma taxa de amostragem de 44.100 Hz, dois canais e amostras de ponto flutuante de 32 bits, minha experiência é que esses blocos têm normalmente o tamanho de 16.384 ou 32.768 bytes, e algumas vezes tão pequenos quanto 12.288 bytes (mas sempre um múltiplo de 4.096), indicando cerca de 35 a 100 milissegundos de áudio cada.

Em seguida a cada chamada do método ReadSample de IMFSource­Reader, um programa pode simplesmente alocar um bloco de memória local, copiar os dados nele e, em seguida, enviar esse bloco local para o objeto IXAudio2SourceVoice com SubmitSourceBuffer.

AudioFilePlayer usa uma abordagem de dois buffers para executar o arquivo: Enquanto um buffer está sendo preenchido com dados, o outro está sendo executado. A Figura 4 mostra o processo inteiro, novamente sem a verificação de erros.

Figura 4 O pipeline de streaming de áudio em StreamMusicFile

void AudioFilePlayer::SubmitBuffer()
{
  // Get the next block of audio data
  int audioBufferLength;
  byte * pAudioBuffer = GetNextBlock(&audioBufferLength);
  if (pAudioBuffer != nullptr)
  {
    // Create an XAUDIO2_BUFFER for submitting audio data
    XAUDIO2_BUFFER buffer = {0};
    buffer.AudioBytes = audioBufferLength;
    buffer.pAudioData = pAudioBuffer;
    buffer.pContext = pAudioBuffer;
    HRESULT hresult = pSourceVoice->SubmitSourceBuffer(&buffer);
  }
}
byte * AudioFilePlayer::GetNextBlock(int * pAudioBufferLength)
{
  // Get an IMFSample object
  ComPtr<IMFSample> mfSample;
  DWORD flags = 0;
  HRESULT hresult = mfSourceReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,
                                               0, nullptr, &flags, nullptr,
                                               &mfSample);
  // Check if we’re at the end of the file
  if (flags & MF_SOURCE_READERF_ENDOFSTREAM)
  {
    endOfFile = true;
    *pAudioBufferLength = 0;
    return nullptr;
  }
  // If not, convert the data to a contiguous buffer
  ComPtr<IMFMediaBuffer> mfMediaBuffer;
  hresult = mfSample->ConvertToContiguousBuffer(&mfMediaBuffer);
  // Lock the audio buffer and copy the samples to local memory
  uint8 * pAudioData = nullptr;
  DWORD audioDataLength = 0;
  hresult = mfMediaBuffer->Lock(&pAudioData, nullptr, &audioDataLength);
  byte * pAudioBuffer = new byte[audioDataLength];
  CopyMemory(pAudioBuffer, pAudioData, audioDataLength);
  hresult = mfMediaBuffer->Unlock();
  *pAudioBufferLength = audioDataLength;
  return pAudioBuffer;
}
// Callback methods from IXAudio2VoiceCallback
void _stdcall AudioFilePlayer::OnBufferEnd(void* pContext)
{
  // Remember to free the audio buffer!
  delete[] pContext;
  // Either submit a new buffer or clean up
  if (!endOfFile)
  {
    SubmitBuffer();
  }
  else
  {
    pSourceVoice->DestroyVoice();
    HRESULT hresult = MFShutdown();
  }
}

Para ter acesso temporário aos dados de áudio, o programa precisa chamar Lock e, em seguida, Unlock no objeto IMFMediaBuffer que representa o novo bloco de dados. Entre essas chamadas, o método GetNextBlock na Figura 4 copia o bloco em uma matriz de bytes recém-alocada.

O método SubmitBuffer na Figura 4 é responsável por definir os campos de uma estrutura XAUDIO2_BUFFER em preparação para o envio dos dados de áudio para execução. Observe como esse método também define o campo pContext como o buffer de áudio alocado. Esse ponteiro é passado para o método de retorno de chamada OnBufferEnd visto próximo ao final da Figura 4, que pode então excluir a memória de matriz.

Quando um arquivo tiver sido completamente lido, a próxima chamada ReadSample definirá um sinalizador MF_SOURCE_READERF_ENDOFSTREAM e o objeto IMFSample será mulo. O programa responde definindo uma variável de campo endOfFile. Nesse momento, o outro buffer ainda está em execução e uma última chamada para OnBufferEnd ocorrerá, que usa a ocasião para liberar alguns recursos do sistema.

Também há um método de retorno de chamada OnStreamEnd que é disparado ao definir o sinalizador XAUDIO2_END_OF_STREAM no XAUDIO2_BUFFER, mas é difícil de usar nesse contexto. O problema é que esse sinalizador não pode ser definido até receber um sinalizador MF_SOURCE_READERF_ENDOFSTREAM da chamada ReadSample. Mas SubmitSourceBuffer não permite buffers nulos ou buffers de tamanho zero, o que significa que você deve enviar um buffer não vazio de qualquer forma, mesmo que não haja mais dados disponíveis.

Metáfora do disco girando

Certamente, passar dados de áudio do Media Foundation para o XAudio2 não é tão fácil quanto usar o Media­Element do Windows 8 e não compensa o esforço, a menos que faça algo interessante com os dados de áudio. Você pode usar o XAudio2 para definir alguns efeitos especiais (como eco ou reverberação). Na próxima edição desta coluna aplicarei os filtros do XAudio2 a arquivos de som.

Enquanto isso, a Figura 5 mostra um programa chamado DeeJay que exibe um disco na tela e o gira conforme a música toca a uma taxa padrão de 33 1/3 revoluções por minuto.

The DeeJay Program
Figura 5 O programa DeeJay

Há uma barra do aplicativo com o botão Carregar Arquivo e dois controles deslizantes, um para volume e outro para controlar a velocidade da reprodução, que não está sendo mostrada. Esse controle deslizante tem valores no intervalo de -3 a 3 e indicam uma taxa de velocidade. O valor padrão é 1. Um valor de 0,5 reproduz o arquivo a meia velocidade, um valor de 3 reproduz o arquivo três vezes mais rápido, um valor de 0 essencialmente pausa a reprodução e valores negativos executam o arquivo de trás para frente (talvez permitindo que você ouça mensagens ocultas codificadas na música).

Como é o Windows 8, certamente você pode também girar o disco com seus dedos, justificando assim o nome do programa. O DeeJay permite a rotação com um só dedo com inércia, de modo que você possa dar ao disco uma boa girada em qualquer direção. Você também pode tocar no disco para mover a “agulha” até um local.

Eu queria muito implementar esse programa de uma forma similar ao projeto StreamMusicFile com chamadas alternadas para ReadSample e SubmitSourceBuffer. Mas apareceram problemas ao tentar executar o arquivo de trás para frente. Eu realmente precisava de IMFSource­Reader para dar suporte a um método ReadPreviousSample, mas ele não dá.

O que IMFSourceReader dá suporte é a um método SetCurrentPosition que permite que você vá para um local anterior no arquivo. No entanto, chamadas de ReadSample subsequentes começam a retornar blocos anteriores a essa posição. Na maior parte do tempo, uma série de chamadas para ReadSample finalmente se encontram no mesmo bloco como a última chamada ReadSample antes de SetCurrentPosition, mas algumas vezes isso não acontece, e isso ficou muito complicado.

No fim eu desisti e o programa simplesmente carrega o arquivo de áudio descompactado inteiro na memória. Para manter o volume de memória baixo, especifiquei amostras de inteiros de 16 bits em vez de amostras de ponto flutuante de 32 bits, mas ainda é cerca de 10MB de memória por minuto de áudio, e carregar um longo movimento de uma sinfonia de Mahler requisitaria cerca de 300MB.

Essas sinfonias de Mahler também exigiram que o método inteiro de carregamento de arquivo fosse executado em um thread secundário, um trabalho que é altamente simplificado pela função create_task disponível no Windows 8.

Para facilitar o trabalho com as amostras individuais, criei uma estrutura simples chamada AudioSample:

 

struct AudioSample
{
  short Left;
  short Right;
};

Assim, em vez de trabalhar com uma matriz de bytes, a classe AudioFilePlayer nesse programa trabalha com uma matriz de valores AudioSample. No entanto, isso significa que o programa está basicamente codificado para arquivos estéreo. Se ele carregar um arquivo de áudio que não tem exatamente dois canais, ele não pode executar esse arquivo.

O método assíncrono de leitura de arquivo armazena os dados obtidos em uma estrutura chamada LoadedAudioFileInfo:

struct LoadedAudioFileInfo
{
  AudioSample* pBuffer;
  int bufferLength;
  WAVEFORMATEX waveFormat;
};

O pBuffer é o grande bloco de memória e bufferLength é o produto da taxa de amostragem (provavelmente 44.100 Hz) e a duração do arquivo em segundos. Essa estrutura é passada diretamente para a classe AudioFilePlayer. Um novo AudioFilePlayer é criado para cada arquivo carregado e substitui qualquer instância anterior de AudioFilePlayer. Para a limpeza, AudioFilePlayer tem um destruidor que exclui a matriz grande que contém o arquivo inteiro, além de duas matrizes menores usadas para enviar buffers para o objeto IXAudio2SourceVoice.

As chaves para executar o arquivo para frente e para trás a várias velocidades são dois campos em AudioFilePlayer do tipo double: audioBuffer­Index e speedRatio. A variável audioBufferIndex aponta para um local dentro da matriz grande que contém o arquivo descompactado inteiro. A variável speedRatio é definida nos mesmos valores que o controle deslizante, -3 até 3. Quando o AudioFilePlayer precisa transferir dados de áudio do buffer grande para os buffers menores para envio, ele incrementa audioBufferIndex de speedRatio para cada amostra. O audioBufferIndex resultante está (em geral) entre duas amostras de arquivo, assim, o método na Figura 6 executa uma interpolação para derivar um valor que é então transferido para o buffer de envio.

Figura 6 Interpolando entre duas amostras no DeeJay

AudioSample AudioFilePlayer::InterpolateSamples()
{
  double left1 = 0, left2 = 0, right1= 0, right2 = 0;
  for (int i = 0; i < 2; i++)
  {
    if (pAudioBuffer == nullptr)
      break;
    int index1 = (int)audioBufferIndex;
    int index2 = index1 + 1;
    double weight = audioBufferIndex - index1;
    if (index1 >= 0 && index1 < audioBufferLength)
    {
      left1 = (1 - weight) * pAudioBuffer[index1].Left;
      right1 = (1 - weight) * pAudioBuffer[index1].Right;
    }
    if (index2 >= 0 && index2 < audioBufferLength)
    {
      left2 = weight * pAudioBuffer[index2].Left;
      right2 = weight * pAudioBuffer[index2].Right;
    }
  }
  AudioSample audioSample;
  audioSample.Left = (short)(left1 + left2);
  audioSample.Right = (short)(right1 + right2);
  return audioSample;
}

A interface de toque

Para manter o programa simples, a interface de toque inteira consiste em um evento Tapped (para posicionar a “agulha” em um local diferente no disco) e três eventos Manipulation: o manipulador ManipulationStarting inicializa a rotação de um só dedo; o manipulador ManipulationDelta define uma taxa de velocidade para o AudioFilePlayer que substitui a taxa de velocidade do controle deslizante; e o manipulador ManipulationCompleted restaura a taxa de velocidade em AudioFilePlayer ao valor do controle deslizante após a conclusão de todos os movimentos de inércia.

Os valores da velocidade de rotação estão disponíveis diretamente de argumentos do evento do manipulador ManipulationDelta. Eles estão em unidades de graus de rotação por milissegundo. Se você considerar que a velocidade de um disco LP padrão de 33 1/3 revoluções por minuto é equivalente a 200° por segundo, ou 0,2° por milissegundo, eu meramente precisei dividir o valor no evento ManipulationDelta por 0,2 para obter a taxa de velocidade que eu necessitava.

No entanto, descobri que as velocidades relatadas pelo ManipulationDelta são bastante irregulares, assim, tive de suavizá-las com uma lógica simples envolvendo uma variável de campo chamada smoothVelocity:

smoothVelocity = 0.95 * smoothVelocity +
                 0.05 * args->Velocities.Angular / 0.2;
pAudioFilePlayer->SetSpeedRatio(smoothVelocity);

Em um toca-discos real, você pode parar a rotação simplesmente pressionando seu dedo no disco. Mas isso não funciona aqui. O movimento real de seu dedo é necessário para que eventos Manipulation sejam gerados, assim, para parar o disco você precisa pressionar e então mover seu dedo (ou mouse ou caneta) um pouco.

A lógica de desaceleração inercial também não corresponde à realidade. Este programa permite que o movimento inercial termine completamente antes de restaurar a taxa de velocidade ao valor indicado pelo controle deslizante. Na realidade, esse valor do controle deslizante deveria exibir um tipo de pull nos valores inerciais, mas isso teria complicado a lógica de maneira considerável.

Além disso, eu não pude realmente detectar um efeito inercial “não natural”. Sem dúvida um DJ de verdade sentiria a diferença imediatamente.

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 aos seguintes especialistas técnicos pela revisão deste artigo: Richard Fricks (Microsoft)