El factor DirectX

Transmisión por secuencias y manipulación de archivos de audio en Windows 8

Charles Petzold

Descargar el ejemplo de código

Muchos usuarios de Windows por estos días tienen una biblioteca de música en sus discos duros con quizás miles o decenas de miles de archivos MP3 y WMA. Para reproducir esta música en el equipo, generalmente estos usuarios ejecutan el reproductor de Windows Media o la aplicación Windows 8 Music Pero para los programadores, es bueno saber que podemos escribir nuestros propios programas para reproducir estos archivos. Windows 8 proporciona interfaces de programación para obtener acceso a la biblioteca de música, obtener información acerca de archivos de música individuales (como artista, título y duración) y reproducir estos archivos mediante MediaElement.

MediaElement es el enfoque sencillo y, desde luego, hay alternativas que dificultan más el trabajo pero que además agregan mucha versatilidad. Con dos componentes DirectX (Media Foundation y XAudio2), es posible que una aplicación se involucre mucho más en este proceso. Puede cargar fragmentos de datos de audio descomprimidos de archivos de música y analizarlos o manipularlos de alguna manera antes (o en lugar de) reproducir la música. ¿Se ha preguntado alguna vez cómo suena una Étude de Chopin cuando se reproduce al revés a media velocidad? Bueno, yo tampoco, pero uno de los programas que acompañan este artículo lo permitirá a averiguarlo.

Selectores y acceso masivo

Ciertamente, la manera más sencilla para que un programa de Windows 8 obtenga acceso a la biblioteca de música es a través de FileOpenPicker, que puede inicializarse en un programa C++ para cargar archivos de audio como este:

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

Llame a PickSingleFileAsync para mostrar FileOpenPicker y deje que el usuario seleccione un archivo.

Para una exploración libre de las carpetas y los archivos, también es posible que el archivo de manifiesto de la aplicación indique si desea más acceso extensivo a la biblioteca de música. El programa puede entonces usar las clases del espacio de nombres Windows::Storage::BulkAccess para enumerar las carpetas y los archivos de música por su cuenta.

Independientemente del enfoque que tome, a cada archivo lo representa un objeto StorageFile. A partir de ese objeto, puede obtener una miniatura, la cual es una imagen de la portada (si existe) del álbum musical. A partir de la propiedad Properties de StorageFile, puede obtener un objeto MusicProperties, el cual proporciona el artista, el álbum, el nombre de la pista, la duración y otra información estándar asociada con el archivo de música.

Al llamar a OpenAsync en StorageFile, también puede abrirlo para lectura y obtener un objeto IRandomAccessStream e incluso leer todo el archivo en memoria. Si es un archivo WAV, podría considerar analizar el archivo, extraer los datos de forma de onda y reproducir el sonido a través de XAudio2, tal como lo describí en entregas recientes de esta columna.

Pero si es un archivo MP3 o WMA, no es tan fácil. Tendrá que descomprimir los datos de audio y ese un trabajo que probablemente no querrá asumir usted mismo. Afortunadamente, las API de Media Foundation incluyen instalaciones para descomprimir archivos MP3 y WMA, y ponerlos datos en una forma que se pueda pasar directamente a XAudio2 para su reproducción.

Otro enfoque para obtener acceso a datos de audio descomprimidos es a través de un efecto de audio que va adjunto a un MediaElement. Espero demostrar esta técnica en un artículo posterior.

Transmisión por secuencias de Media Foundation

Para usar las funciones e interfaces de Media Foundation que analizaré aquí, tendrá que vincular su programa de Windows 8 con las bibliotecas mfplat.lib y mfreadwrite.lib, y deberá incluir instrucciones para mfapi.h, mfidl.h y mfreadwrite.h en su archivo pch.h. (Además, asegúrese de incluir initguid.h antes de mfapi.h u obtendrá errores de vínculo que podrían dejarlo confundido durante muchas horas improductivas). Si además va a usar XAudio2 para reproducir los archivos (como lo haré yo aquí), necesitará la biblioteca de importación xaudio2.lib y el archivo de encabezado xaudio2.h.

Entre el código descargable para esta columna está un proyecto de Windows 8 denominado StreamMusicFile que demuestra prácticamente el código mínimo necesario para cargar un archivo desde la biblioteca de música del equipo, descomprimirlo a través de Media Foundation y reproducirlo con XAudio2. Un botón invoca FileOpenPicker y después de que ha seleccionado un archivo, el programa muestra alguna información estándar (como se ve en la figura 1) e inmediatamente comienza a reproducir el archivo. De manera predeterminada, el control deslizante de volumen en la parte inferior se configura en 0 y tendrá que aumentarlo para escuchar algo. No hay manera de pausar ni detener el archivo, excepto por finalizar el programa o llamar a otro programa al primer plano.

The StreamMusicFile Program Playing a Music File
Figura 1 Programa StreamMusicFile reproduciendo un archivo de música

De hecho, el programa no deja de reproducir un archivo de música ni siquiera si hace clic en el botón y carga un segundo archivo. En su lugar, descubrirá que ambos archivos se reproducen al mismo tiempo, pero probablemente no en ningún tipo de sincronización coherente. De manera que eso es algo que este programa puede hacer que la aplicación Windows 8 Music y el reproductor multimedia no pueden hacer: ¡reproducir archivos de música al mismo tiempo!

El método que aparece en la figura 2 muestra cómo el programa usa un IRandomAccessStream a partir de un StorageFile para crear un objeto IMFSourceReader capaz de leer un archivo de audio y entregar fragmentos de datos de audio descomprimidos.

Figura 2 Creación e inicialización de un 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;
}

Por claridad, la figura 2 excluye todo el código que trata con los valores de devolución HRESULT errantes. El código verdadero arroja excepciones de tipo COMException, pero el programa no las atrapa como lo haría una aplicación verdadera.

En resumen, este método usa IRandomAccessStream para crear un objeto IMFByteStream, al encapsular la secuencia de entrada, y luego la usa para crear un IMFSourceReader, el cual puede realizar la verdadera descompresión.

Observe el uso de un objeto IMFAttributes para especificar una operación de latencia baja. Esto no es estrictamente obligatorio y puede configurar el segundo argumento en la función MFCreateSourceReaderFromByteStream como nullptr. Sin embargo, mientras el archivo se lee y reproduce, se obtiene acceso al disco duro y usted no quiere que esas operaciones de disco creen vacíos audibles en la reproducción. Si este problema lo pone realmente nervioso, podría considerar leer todo el archivo en un objeto InMemoryRandomAccessStream y usarlo para crear IMFByteStream.

Cuando un programa usa Media Foundation para descomprimir un archivo de audio, el programa no tiene control sobre la tasa de muestreo de los datos descomprimidos que recibe del archivo o del número de canales. Esto lo gobierna el archivo. Sin embargo, el programa puede especificar que las muestras estén en uno de dos formatos diferentes: enteros de 16 bits (usados para audio CD) o valores de punto de flotación de 32 bits (el tipo de C flotante). Internamente, XAudio2 usa muestras de punto flotante de 32 bits, de manera que se requieren menos conversiones internas si se pasan muestras de punto flotante de 32 bits a XAudio2 para reproducir el archivo. Decidí ir por esa ruta en este programa. En consecuencia, el método de la figura 2 especifica el formato de los datos de audio que desea con los dos identificadores MFMediaType_Audio y MFAudioFormat_Float. Si se requieren datos descomprimidos, la única alternativa a este segundo identificador es MFAudioFormat_PCM para muestras de enteros de 16 bits.

En este punto, tenemos un objeto de tipo IMFSourceReader preparado para leer y descomprimir fragmentos de un archivo de audio.

Reproducción del archivo

Originalmente quería tener todo el código para este primer programa en la clase MainPage, pero también quería usar una función de llamada XAudio2. Ese es un problema porque (como descubrí) un tipo de Windows en tiempo de ejecución como MainPage no puede implementar una interfaz que no es de Windows en tiempo de ejecución como IXAudio2VoiceCallback, de manera que necesité una segunda clase, a la que llamé AudioFilePlayer.

Después de obtener un objeto IMFSourceReader a partir del método que aparece en la figura 2, MainPage crea un nuevo objeto AudioFilePlayer y le pasa además un objeto IXAudio2 creado en el constructor MainPage:

new AudioFilePlayer(pXAudio2, mfSourceReader);

De allí, el objeto AudioFilePlayer queda completamente por su cuenta y prácticamente autocontenido. Así es cómo el programa puede reproducir varios archivos al mismo tiempo.

Para reproducir el archivo de música, AudioFilePlayer debe crear un objeto IXAudio2­SourceVoice. Esto requiere una estructura WAVEFORMATEX que indique el formato de los datos de audio que se pasa a la voz de origen y que debe ser coherente con los datos de audio que entrega el objeto IMFSourceReader. Probablemente puede adivinar los parámetros correctos (como dos canales y una velocidad de muestreo de 44.100 Hz) y si obtiene una velocidad de muestreo equivocada, XAudio2 puede realizar las conversiones de velocidad de manera interna. Aun así, lo mejor es obtener una estructura WAVEFORMATEX a partir del IMFSourceReader y usar esa, tal como se muestra en el constructor AudioFilePlayer en la figura 3.

Figura 3 El constructor AudioFilePlayer en 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;
}

Obtener esa estructura WAVEFORMATEX es un pequeño fastidio que implica un bloque de memoria que luego debe alimentarse explícitamente, pero hacia la conclusión del constructor AudioFilePlayer, el archivo está listo para reproducirse.

Para mantener la superficie de memoria de semejante programa al mínimo, el archivo debe leerse y reproducirse en pequeños fragmentos. Tanto Media Foundation como XAudio2 son muy conducentes a este enfoque. Cada llamada al método ReadSample del objeto IMFSourceReader obtiene acceso al siguiente bloque de datos descomprimidos hasta que el archivo se ha leído por completo. Para una velocidad de muestreo de 44.100 Hz, dos canales y ejemplos de puntos flotantes de 32 bits, mi experiencia es que el tamaño de estos bloques suele ser de 16.384 o 32.768 bytes y algunas veces tan pequeños como 12.228 bytes (pero siempre un múltiplo de 4.096), lo cual indica alrededor de 35 a 100 milisegundos de audio cada uno.

Al seguir cada llamada al método ReadSample de IMFSource­Reader, un programa puede sencillamente asignar un bloque de memoria local, copiarle los datos y luego enviar ese bloque local al objeto IXAudio2SourceVoice con SubmitSourceBuffer.

AudioFilePlayer usa un enfoque de dos búferes para reproducir el archivo: Mientras un búfer se llena de datos, el otro está reproduciendo. La figura 4 muestra todo el proceso, nuevamente sin las comprobaciones de errores.

Figura 4 La canalización de la transmisión por secuencias de audio en 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 obtener acceso temporal a los datos de audio, el programa debe llamar a Lock y luego a Unlock en un objeto IMFMediaBuffer que representa el nuevo bloque de datos. Ente esas llamadas, el método GetNextBlock en la figura 4 copia el bloque en una nueva matriz de bytes asignados recientemente.

El método SubmitBuffer en la figura 4 es responsable de configurar los campos de una estructura XAUDIO2_BUFFER en preparación para enviar los datos de audio para reproducción. Observe cómo este método también configura el campo pContext en el búfer de audio asignado. Este puntero se pasa al método de llamada OnBufferEnd que se ve hacia el final de la figura 4, el cual luego puede borrar la memoria de la matriz.

Cuando un archivo se ha leído por completo, la siguiente llamada ReadSample configura una marca MF_SOURCE_READERF_ENDOFSTREAM y el objeto IMFSample es nulo. El programa responde al configurar una variable de campo endOfFile. En este momento, el otro búfer aún está en ejecución y habrá una última llamada a OnBufferEnd, lo cual aprovecha la ocasión para liberar algunos recursos del sistema.

También hay un método de llamada OnStreamEnd que se activa al configurar la marca XAUDIO2_END_OF_STREAM en XAUDIO2_BUFFER, pero es difícil de usar en este contexto. El problema es que no puede configurar esa marca hasta que reciba una marca MF_SOURCE_READERF_ENDOFSTREAM de la llamada ReadSample. Pero SubmitSourceBuffer no permite búferes nulos ni búferes sin tamaño, lo cual significa que tiene que enviar de todas maneras un búfer que no esté vacío, ¡aun cuando no haya más datos disponibles!

La metáfora de girar un disco

Desde luego, pasar datos de audio de Media Foundation a XAudio2 no es ni siquiera tan fácil como usar Media­Element de Windows 8 y apenas si vale la pena el esfuerzo, a menos que vaya a hacer algo interesante con los datos de audio. Puede usar XAudio2 para configurar algunos efectos especiales (como eco o reverberación) y en las próximas entregas de esta columna aplicaré filtros XAudio2 a los archivos de sonido.

Entretanto, en la figura 5 se aprecia un programa denominado DeeJay que muestra un disco en pantalla y lo gira mientras la música se reproduce a una velocidad predeterminada de 33 1/3 revoluciones por minuto.

The DeeJay Program
Figura 5 Programa DeeJay

No se muestra una barra de aplicaciones con un botón Cargar archivo y dos controles deslizantes: uno para el volumen y otro para controlar la velocidad de reproducción. Este control deslizante tiene valores que van de -3 a 3 e indica una relación de velocidad. El valor predeterminado es 1. Un valor de 0,5 reproduce el archivo a velocidad media, un valor de 3 reproduce el archivo tres veces más rápido, un valor de 0 en esencia pone en pausa la reproducción y los valores negativos reproducen el archivo a la inversa (lo cual quizás le permite escuchar mensajes ocultos codificados en el música).

Como se trata de Windows 8, desde luego también puede girar el disco con los dedos, de allí el nombre del programa. DeeJay permite la rotación con un solo dedo con inercia, para que pueda girar bien el disco en cualquier dirección. También puede pulsar el disco para mover la "aguja" a esa ubicación.

Tenía muchas, pero muchas ganas de implementar este programa de manera similar al proyecto StreamMusicFile con llamadas alternantes a ReadSample y SubmitSourceBuffer. Pero surgieron problemas al intentar reproducir el archivo al revés. Realmente necesitaba que IMFSource­Reader admitiera un método ReadPreviousSample, pero no lo hace.

Lo que IMFSourceReader sí admite es un método SetCurrentPosition que le permite pasar a una ubicación anterior en el archivo. Sin embargo, posteriores llamadas a ReadSample comienzan a devolver bloques anteriores a esa posición. La mayor parte del tiempo, una serie de llamadas a ReadSample finalmente se juntan en el mismo bloque como la última llamada a ReadSample antes de SetCurrentPosition, pero a veces no lo hacen y eso complica bastante las cosas.

Al final, me di por vencido y el programa simplemente carga todo el archivo de audio descomprimido en la memoria. Para no atiborrar la superficie de la memoria, especifiqué ejemplos de enteros de 16 bits en lugar de ejemplos de punto flotante de 32 bits, pero aun así son alrededor de 10MB de memoria por minuto de audio y cargar un movimiento largo de una sinfonía de Mahler requisaría unos 300MB.

Esas sinfonías de Mahler también obligaban a que todo el método de carga del archivo se ejecutara en un subproceso secundario, un trabajo que se simplifica muchísimo gracias a la función create_task disponible en Windows 8.

Para facilitar el trabajo con los ejemplos individuales, creé una estructura sencilla denominada AudioSample:

struct AudioSample
{
  short Left;
  short Right;
};

De manera que en lugar de trabajar con una matriz de bytes, la clase AudioFilePlayer de este programa funciona con una matriz de valores AudioSample. Sin embargo, esto significa que el programa se codifica de forma rígida básicamente para archivos estéreo. Si carga un archivo de audio que no tenga exactamente dos canales, ¡no puede reproducir ese archivo!

El método asincrónico de lectura de archivo almacena los datos que obtiene en una estructura que llamo LoadedAudioFileInfo:

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

pBuffer es el bloque grande de memoria y bufferLength es el producto de la velocidad de muestreo (probablemente 44.100 Hz) y la duración del archivo en segundos. Esta estructura se pasa directamente a la clase AudioFilePlayer. Se crea un nuevo AudioFilePlayer para cada archivo cargado y reemplaza cualquier instancia AudioFilePlayer anterior. Para el borrado, AudioFilePlayer tiene un destructor que elimina la matriz grande que contiene todo el archivo, así como dos matrices más pequeñas para enviar búferes al objeto IXAudio2SourceVoice.

Las claves para reproducir el archivo hacia delante y atrás a diversas velocidades son dos campos en AudioFilePlayer de tipo doble: audioBuffer­Index y speedRatio. La variable audioBufferIndex apunta a una ubicación dentro de la matriz grande que contiene todo el archivo descomprimido. La variable speedRatio se configura en los mismos valores que el control deslizante, -3 a 3. Cuando AudioFilePlayer necesita transferir datos de audio desde el búfer grande a búferes más pequeños para envío, aumenta audioBufferIndex por speedRatio para cada ejemplo. El audioBufferIndex resultante está (en general) entre dos ejemplos de archivo, de manera que el método en la figura 6 realiza una interpolación para derivar un valor que luego se transfiere al búfer de envío.

Figura 6 Interpolación entre dos ejemplos en 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;
}

La interfaz táctil

Para mantener simple el programa, toda la interfaz táctil consta de un evento pulsado (para colocar la "aguja" en una ubicación diferente en el disco) y tres eventos de manipulación: el controlador ManipulationStarting inicializa la rotación con un solo dedo; el controlador ManipulationDelta configura una relación de velocidad para AudioFilePlayer que anula la relación de velocidad del control deslizante; y el controlador ManipulationCompleted restaura la relación de velocidad en AudioFilePlayer al valor de control deslizante después de que se ha completado todo el movimiento por inercia.

Los valores de velocidad de giro están directamente disponibles desde argumentos de eventos del controlador ManipulationDelta. Estos están en unidades de grados de rotación por milisegundo. Si considera que una velocidad de disco de larga duración estándar de 33 1/3 revoluciones por minuto es equivalente a 200 º por segundo, o 0,2 º por milisegundo, simplemente tuve que dividir el valor en el evento ManipulationDelta por 0,2 para obtener la relación de velocidad que requería.

Sin embargo, descubrí que las velocidades informadas por ManipulationDelta son bastante erráticas, así que tuve que simplificarlas con un poco de lógica sencilla que implica una variable de campo denominada smoothVelocity:

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

En un tocadiscos de verdad, puede detener la rotación con solo presionar el dedo en el disco. Pero eso no funciona aquí. El movimiento real del dedo es necesario para que se generen eventos de manipulación, de manera que para detener el disco debe presionar y luego mover el dedo (o mouse o lápiz) un poco.

La lógica de la desaceleración por inercia tampoco coincide con la realidad. Este programa permite que el movimiento por inercia finalice por completo antes de restaurar la relación de velocidad al valor indicado por el control deslizante. En realidad, ese valor del control deslizante debe exhibir un tipo de extracción sobre los valores de inercia, pero eso habría complicado la lógica considerablemente.

Además, en realidad no pude detectar un efecto de inercia "poco natural". Sin duda un DJ de verdad sentiría la diferencia de inmediato.

Charles Petzold ha colaborado durante largo tiempo con MSDN Magazine y es el autor de “Programming Windows, 6th edition” (O’Reilly Media, 2012), un libro sobre cómo escribir aplicaciones para Windows 8. Su sitio web es charlespetzold.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Richard Fricks (Microsoft)