Facteur DirectX

Diffusion et manipulation des fichiers audio dans Windows 8

Charles Petzold

Télécharger l'exemple de code

Charles PetzoldDe nos jours, de nombreux utilisateurs de Windows ont sur leur disque dur une médiathèque susceptible de contenir des milliers ou des dizaines de milliers de fichiers MP3 et WMA. Pour écouter cette musique sur leur PC, ces utilisateurs exécutent généralement Windows Media Player ou l'application Musique de Windows 8. Mais pour les programmeurs, il est bon de savoir que nous pouvons écrire nos propres programmes pour lire ces fichiers. Windows 8 fournit des interfaces de programmation qui permettent d'accéder à la médiathèque, d'obtenir des informations sur chaque fichier de musique (notamment l'artiste, le titre et la durée) et de lire ces fichiers à l'aide de MediaElement.

MediaElement est l'approche simple et, bien sûr, il existe des alternatives qui compliquent le travail mais ajoutent également une grande polyvalence. Avec deux composants DirectX, Media Foundation et XAudio2, il est possible d'impliquer une application bien davantage dans ce processus. Vous pouvez charger des blocs de données audio décompressées provenant de fichiers de musique et analyser ces données ou les manipuler d'une façon ou d'une autre avant (ou au lieu) d'écouter la musique. Vous êtes-vous déjà demandé à quoi ressemble une Étude de Chopin lorsqu'elle est écoutée à l'envers à demi-vitesse ? En fait, moi non plus, mais l'un des programmes inclus dans cet article va vous permettre de le découvrir.

Sélecteurs et accès en masse

Pour un programme Windows 8, la manière la plus simple d'accéder à la médiathèque est d'utiliser la classe FileOpenPicker, qui peut être initialisée dans un programme C++ pour charger des fichiers audio comme suit :

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

Appelez PickSingleFileAsync pour afficher la classe FileOpenPicker et permettre à l'utilisateur de sélectionner un fichier.

Pour une exploration libre des dossiers et fichiers, il est également possible pour le fichier manifeste de l'application d'indiquer qu'il souhaite un accès plus complet à la médiathèque. Le programme peut ensuite utiliser les classes de l'espace de noms Windows::Storage::BulkAccess pour énumérer tout seul les dossiers et les fichiers de musique.

Quelle que soit l'approche que vous choisissez, chaque fichier est représenté par un objet StorageFile. À partir de cet objet, vous pouvez obtenir une miniature, qui est une image de la couverture de l'album musical (le cas échéant). À partir de la propriété Properties de StorageFile, vous pouvez obtenir un objet MusicProperties, qui fournit l'artiste, l'album, le nom de la piste, la durée et les autres informations standard associées au fichier de musique.

En appelant OpenAsync sur cet objet StorageFile, vous pouvez également l'ouvrir pour lecture et obtenir un objet IRandomAccessStream, et même lire le fichier entier en mémoire. S'il s'agit d'un fichier WAV, vous pouvez envisager d'analyser le fichier, d'extraire les données de forme d'onde et de lire le son via XAudio2, comme je l'ai décrit dans les récents articles de cette rubrique.

Mais s'il s'agit d'un fichier MP3 ou WMA, ce n'est pas si simple. Vous devrez décompresser les données audio et c'est là un travail dont vous ne voudrez probablement pas vous charger vous-même. Heureusement, les API de Media Foundation comprennent des fonctionnalités de décompression des fichiers MP3 et WMA et de conversion des données dans un format pouvant être transmis directement à XAudio2 en vue de leur lecture.

Une autre approche permettant d'obtenir l'accès aux données audio décompressées consiste à utiliser un effet audio associé à un objet MediaElement. J'espère faire la démonstration de cette technique dans un prochain article.

Diffusion en continu Media Foundation

Pour utiliser les fonctions et les interfaces Media Foundation dont je vais parler ici, vous devrez lier votre programme Windows 8 aux bibliothèques d'importation mfplat.lib et mfreadwrite.lib, et vous aurez besoin d'instructions #include pour mfapi.h, mfidl.h et mfreadwrite.h dans votre fichier pch.h. Veillez également à inclure initguid.h avant mfapi.h, sinon vous obtiendrez des erreurs de lien susceptibles de vous laisser perplexe pendant de nombreuses heures improductives. Si vous utilisez également XAudio2 pour lire les fichiers (comme je vais le faire ici), vous aurez besoin de la bibliothèque d'importation xaudio2.lib et du fichier d'en-tête xaudio2.h.

Le code téléchargeable de cet article comprend un projet Windows 8 nommé StreamMusicFile qui démontre assez bien le code minimal nécessaire pour charger un fichier à partir de la médiathèque du PC, le décompresser via Media Foundation et le lire via XAudio2. Un bouton appelle FileOpenPicker et, une fois que vous avez sélectionné un fichier, le programme affiche quelques informations standard (comme illustré dans la figure 1) et commence immédiatement à lire le fichier. Par défaut, le curseur du volume situé en bas est défini sur 0, si bien que vous devrez augmenter sa valeur pour pouvoir entendre quelque chose. Il n'y a aucun moyen d'interrompre ou d'arrêter le fichier, sauf en arrêtant le programme ou en faisant passer un autre programme au premier plan.

The StreamMusicFile Program Playing a Music File
Figure 1 Programme StreamMusicFile lisant un fichier de musique

En fait, le programme n'arrête pas de lire un fichier de musique même si vous cliquez sur le bouton et chargez un second fichier. À la place, vous découvrirez que les deux fichiers sont lus en même temps, mais probablement sans aucune synchronisation cohérente. Voilà quelque chose que ce programme peut faire, contrairement à l'application Musique de Windows 8 et Media Player : lire plusieurs fichiers de musique simultanément !

La méthode présentée dans la figure 2 montre comment le programme utilise un objet IRandomAccessStream à partir d'un objet StorageFile pour créer un objet IMFSourceReader capable de lire un fichier audio et de fournir des blocs de données audio décompressées.

Figure 2 Création et initialisation d'un objet 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;
}

Par souci de clarté, la figure 2 exclut tout le code qui gère les valeurs de retour HRESULT disséminées. Le code réel déclenche des exceptions de type COMException, mais le programme n'intercepte pas ces exceptions comme le ferait une application réelle.

En bref, cette méthode utilise l'objet IRandomAccessStream pour créer un objet IMFByteStream qui encapsule le flux d'entrée, puis utilise celui-ci pour créer un objet IMFSourceReader qui peut effectuer la décompression réelle.

Vous remarquerez l'utilisation d'un objet IMFAttributes pour spécifier une opération à faible latence. Ce n'est pas strictement requis et vous pouvez affecter au second argument de la fonction MFCreateSourceReaderFromByteStream la valeur nullptr. Toutefois, étant donné que le fichier est en cours de lecture, le programme accède au disque dur et vous ne voulez pas que ces opérations sur le disque créent des blancs audibles dans la lecture. Si ce problème vous inquiète vraiment, vous pouvez envisager de lire l'intégralité du fichier dans un objet InMemoryRandomAccessStream et d'utiliser celui-ci pour créer l'objet IMFByteStream.

Lorsqu'un programme utilise Media Foundation pour décompresser un fichier audio, il n'a aucun contrôle sur le taux d'échantillonnage des données décompressées qu'il reçoit du fichier, ni sur le nombre de canaux. Ces paramètres sont régis par le fichier. Toutefois, le programme peut spécifier que les échantillons doivent être dans l'un des deux formats différents possibles : entiers 16 bits (format utilisé pour les CD audio) ou valeurs 32 bits à virgule flottante (type float C). En interne, XAudio2 utilise des échantillons 32 bits à virgule flottante, si bien que moins de conversions internes sont requises si des échantillons 32 bits à virgule flottante sont transmis à XAudio2 pour lire le fichier. J'ai donc décidé de suivre cette voie dans ce programme. En conséquence, la méthode de la figure 2 spécifie le format des données audio qu'elle souhaite avec les deux identificateurs MFMediaType_Audio et MFAudioFormat_Float. Si des données décompressées sont requises, la seule alternative à ce second identificateur est MFAudioFormat_PCM pour les échantillons entiers 16 bits.

À ce stade, nous avons un objet de type IMFSourceReader prêt à lire et décompresser les blocs d'un fichier audio.

Lecture du fichier

Je souhaitais initialement que tout le code de ce premier programme soit contenu dans la classe MainPage, mais je souhaitais également utiliser une fonction de rappel XAudio2. C'est un problème car (comme je l'ai découvert) un type Windows Runtime tel que MainPage ne peut pas implémenter une interface non-Windows Runtime telle que IXAudio2VoiceCallback, si bien que j'ai eu besoin d'une seconde classe, que j'ai appelée AudioFilePlayer.

Après avoir obtenu un objet IMFSourceReader de la méthode présentée dans la figure 2, MainPage crée un nouvel objet AudioFilePlayer en lui transmettant également un objet IXAudio2 créé dans le constructeur MainPage :

new AudioFilePlayer(pXAudio2, mfSourceReader);

À partir de là, l'objet AudioFilePlayer est entièrement seul et à peu près autonome. Voilà comme le programme peut lire plusieurs fichiers simultanément.

Pour lire le fichier de musique, AudioFilePlayer doit créer un objet IXAudio2­SourceVoice. Cela exige une structure WAVEFORMATEX indiquant le format des données audio à transmettre à la voix source et qui doit être cohérente avec les données audio fournies par l'objet IMFSourceReader. Vous pouvez probablement deviner les paramètres corrects (par exemple, deux canaux et un taux d'échantillonnage de 44 100 Hz) et si vous obtenez un taux d'échantillonnage incorrect, XAudio2 peut effectuer des conversions de taux d'échantillonnage en interne. Il est quand même préférable d'obtenir une structure WAVEFORMATEX de l'objet IMFSourceReader et de l'utiliser, comme illustré dans le constructeur AudioFilePlayer de la figure 3.

Figure 3 Constructeur AudioFilePlayer dans 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;
}

L'obtention de cette structure WAVEFORMATEX est ennuyeuse et implique un bloc de mémoire qui doit ensuite être explicitement libéré, mais à la fin du constructeur AudioFilePlayer, le fichier est prêt à être lu.

Pour que l'encombrement mémoire d'un tel programme soit aussi minime que possible, le fichier doit être lu par petits blocs. Media Foundation et XAudio2 sont tous deux très compatibles avec cette approche. Chaque appel de la méthode ReadSample de l'objet IMFSourceReader obtient l'accès au bloc de données décompressées suivant jusqu'à ce que le fichier soit intégralement lu. Pour un taux d'échantillonnage de 44 100 Hz, deux canaux et des échantillons 32 bits à virgule flottante, mon expérience est que ces blocs sont généralement d'une taille de 16 384 ou 32 768 octets, parfois même aussi peu que 12 288 octets (mais toujours un multiple de 4 096), ce qui correspond à environ 35 à 100 millisecondes de contenu audio chacun.

À la suite de chaque appel de la méthode ReadSample de l'objet IMFSource­Reader, un programme peut simplement allouer un bloc de mémoire local, y copier les données, puis envoyer ce bloc local à l'objet IXAudio2SourceVoice avec SubmitSourceBuffer.

AudioFilePlayer utilise une approche à deux mémoires tampons pour lire le fichier : tandis qu'une mémoire tampon est remplie de données, l'autre mémoire tampon les lit. La figure 4 montre l'intégralité du processus, une fois encore sans les contrôles des erreurs.

Figure 4 Pipeline de diffusion audio en continu dans 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();
  }
}

Pour obtenir un accès temporaire aux données audio, le programme doit appeler Lock puis Unlock sur un objet IMFMediaBuffer représentant le nouveau bloc de données. Entre ces appels, la méthode GetNextBlock de la figure 4 copie le bloc dans un tableau d'octets nouvellement alloué.

La méthode SubmitBuffer de la figure 4 est responsable de la préparation des champs d'une structure XAUDIO2_BUFFER pour soumettre les données audio à lire. Observez comment cette méthode définit également le champ pContext sur la mémoire tampon audio allouée. Ce pointeur est transmis à la méthode de rappel OnBufferEnd affichée vers la fin de la figure 4, qui peut ensuite supprimer la mémoire du tableau.

Lorsqu'un fichier a été entièrement lu, l'appel suivant de ReadSample définit un indicateur MF_SOURCE_READERF_ENDOFSTREAM et l'objet IMFSample est Null. Le programme répond en définissant une variable de champ endOfFile. À ce stade, l'autre mémoire tampon lit toujours les données et un dernier appel de OnBufferEnd a lieu, qui utilise l'occasion pour libérer certaines ressources système.

Il existe également une méthode de rappel OnStreamEnd qui est déclenchée en définissant l'indicateur XAUDIO2_END_OF_STREAM dans la structure XAUDIO2_BUFFER, mais elle est difficile à utiliser dans ce contexte. Le problème, c'est que vous ne pouvez pas définir cet indicateur avant d'avoir reçu un indicateur MF_SOURCE_READERF_ENDOFSTREAM de l'appel de ReadSample. Mais SubmitSourceBuffer n'autorise pas les mémoires tampons Null ni les mémoires tampons d'une taille égale à zéro, ce qui signifie que vous devez toujours soumettre une mémoire tampon non vide, même s'il n'y a plus aucune donnée disponible !

Rotation d'une métaphore de disque

Bien sûr, la transmission des données audio de Media Foundation à XAudio2 n'est pas aussi simple que l'utilisation de la classe Media­Element de Windows 8 et ne vaut pas vraiment la peine d'y consacrer des efforts sauf si vous voulez faire quelque chose d'intéressant avec les données audio. Vous pouvez utiliser XAudio2 pour définir certains effets spéciaux (comme l'effet de reverb ou d'écho) et, dans le prochain article de cette rubrique, j'appliquerai des filtres XAudio2 à des fichiers audio.

En attendant, la figure 5 montre un programme nommé DeeJay qui affiche un disque à l'écran et le fait tourner alors que la musique est lue à une vitesse par défaut de 33 tours 1/3 par minute.

The DeeJay Program
Figure 5 Programme DeeJay

Cette figure ne montre pas une barre d'application avec un bouton Charger le fichier et deux curseurs : un pour le volume et l'autre pour contrôler la vitesse de lecture. Les valeurs de ce curseur vont de -3 à 3 et indiquent un rapport de vitesse. La valeur par défaut est 1. La valeur 0,5 lit le fichier à demi-vitesse, la valeur 3 le lit trois fois plus vite que la normale, la valeur 0 met essentiellement la lecture en pause, et les valeurs négatives lisent le fichier à l'envers (ce qui peut éventuellement vous permettre d'entendre des messages cachés codés dans la musique).

Étant donné qu'il s'agit de Windows 8, vous pouvez bien sûr également faire tourner le disque avec vos doigts, ce qui justifie le nom du programme. DeeJay permet une rotation d'un seul doigt avec inertie. Vous pouvez donc faire fortement tourner le disque dans un sens ou dans l'autre. Vous pouvez également taper sur le disque pour placer « l'aiguille » à cet emplacement.

J'avais vraiment très très envie d'implémenter ce programme d'une manière similaire au projet StreamMusicFile avec des appels alternés de ReadSample et SubmitSourceBuffer. Mais des problèmes sont survenus lorsque j'ai tenté de lire le fichier à l'envers. J'avais vraiment besoin que l'objet IMFSource­Reader prenne en charge une méthode ReadPreviousSample, mais ce n'est pas le cas.

Ce que l'objet IMFSourceReader prend en charge, c'est une méthode SetCurrentPosition qui vous permet de passer à un emplacement précédent du fichier. Cependant, les appels suivants de ReadSample commencent à retourner des blocs situés avant cette position. La plupart du temps, une série d'appels de ReadSample finit par se retrouver au même bloc que le dernier appel de ReadSample avant SetCurrentPosition, mais pas toujours, et cela rend cette méthode vraiment trop compliquée.

J'ai fini par abandonner et le programme charge simplement le fichier audio décompressé entier dans la mémoire. Pour limiter l'encombrement mémoire, j'ai spécifié des échantillons entiers 16 bits au lieu d'échantillons 32 bits à virgule flottante, mais cela fait encore environ 10 Mo de mémoire par minute de contenu audio, et le chargement d'un long mouvement d'une symphonie de Mahler réquisitionnerait environ 300 Mo.

Ces symphonies de Mahler imposaient également que la méthode de chargement du fichier entier soit exécutée dans un thread secondaire, une tâche grandement simplifiée par la fonction create_task disponible dans Windows 8.

Pour faciliter l'utilisation des échantillons individuels, j'ai créé une structure simple nommée AudioSample :

struct AudioSample
{
  short Left;
  short Right;
};

Par conséquent, au lieu d'utiliser un tableau d'octets, la classe AudioFilePlayer de ce programme utilise un tableau de valeurs AudioSample. Cependant, cela signifie que le programme est essentiellement codé en dur pour les fichiers stéréo. S'il charge un fichier audio qui n'a pas exactement deux canaux, il ne peut pas le lire !

La méthode de lecture de fichier asynchrone stocke les données qu'elle obtient dans une structure que j'ai appelée LoadedAudioFileInfo :

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

pBuffer est le gros bloc de mémoire et bufferLength est le produit du taux d'échantillonnage (probablement 44 100 Hz) et de la durée du fichier en secondes. Cette structure est transmise directement à la classe AudioFilePlayer. Une nouvelle classe AudioFilePlayer est créée pour chaque fichier chargé et elle remplace toute instance précédente d'AudioFilePlayer. Pour le nettoyage, AudioFilePlayer a un destructeur qui supprime le grand tableau contenant l'intégralité du fichier, ainsi que deux tableaux plus petits utilisés pour envoyer les mémoires tampons à l'objet IXAudio2SourceVoice.

Les clés permettant de lire le fichier à l'endroit et à l'envers à différentes vitesses sont deux champs de type double de la classe AudioFilePlayer : audioBuffer­Index et speedRatio. La variable audioBufferIndex pointe vers un emplacement du grand tableau contenant l'intégralité du fichier décompressé. La variable speedRatio prend les mêmes valeurs que le curseur, c'est-à-dire -3 à 3. Lorsque la classe AudioFilePlayer doit transférer des données audio de la grande mémoire tampon vers les plus petites mémoires tampons en vue de leur envoi, elle incrémente audioBufferIndex de la valeur de speedRatio pour chaque échantillon. La valeur audioBufferIndex obtenue se situe (en général) entre deux échantillons du fichier, si bien que la méthode de la figure 6 effectue une interpolation pour dériver une valeur qui est ensuite transférée à la mémoire tampon d'envoi.

Figure 6 Interpolation entre deux échantillons dans 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;
}

Interface tactile

Pour que le programme reste simple, l'intégralité de l'interface tactile consiste en un événement Tapped (pour positionner « l'aiguille » à un autre emplacement sur le disque) et trois événements Manipulation : le gestionnaire ManipulationStarting initialise la rotation d'un seul doigt, le gestionnaire ManipulationDelta définit un rapport de vitesse pour la classe AudioFilePlayer qui prévaut sur le rapport de vitesse du curseur, et le gestionnaire ManipulationCompleted restaure la valeur du curseur comme rapport de vitesse dans AudioFilePlayer une fois que le mouvement inertiel est entièrement terminé.

Les valeurs de vitesse rotationnelle sont directement disponibles à partir des arguments d'événement du gestionnaire ManipulationDelta. Elles sont indiquées en unités de degrés de rotation par milliseconde. Si vous considérez que la vitesse de lecture standard d'un disque 33 tours, soit 33 tours 1/3 par minute, est équivalente à 200° par seconde, ou 0,2° par milliseconde, j'ai tout simplement dû diviser la valeur de l'événement ManipulationDelta par 0,2 pour obtenir le rapport de vitesse dont j'avais besoin.

Toutefois, j'ai découvert que les vitesses rapportées par l'événement ManipulationDelta sont assez irrégulières et j'ai donc dû les lisser à l'aide d'une logique simple impliquant une variable de champ nommée smoothVelocity :

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

Sur une platine réelle, vous pouvez arrêter la rotation d'une simple pression du doigt sur le disque. Mais cela ne fonctionne pas ici. Le mouvement réel de votre doigt est nécessaire pour générer les événements Manipulation. Par conséquent, pour arrêter le disque, vous devez appuyer votre doigt (ou le stylet ou cliquer sur la souris), puis le déplacer un peu.

La logique de décélération inertielle ne correspond pas non plus à la réalité. Ce programme permet aux mouvements inertiels de finir entièrement avant de restaurer le rapport de vitesse à la valeur indiquée par le curseur. En réalité, cette valeur du curseur devrait exercer une certaine force sur les valeurs inertielles, mais cela aurait considérablement compliqué la logique.

Du reste, je n'ai réellement pu détecter aucun effet d'inertie « non-naturel ». Mais indubitablement, un vrai DJ sentirait tout de suite la différence.

Charles Petzold contribue depuis longtemps à l'élaboration de MSDN Magazine et est l'auteur du livre « Programming Windows, 6th edition » (O’Reilly Media, 2012) qui traite de l'écriture d'applications pour Windows 8. Son site Web est charlespetzold.com.

Merci aux experts techniques suivants d'avoir relu cet article : Richard Fricks (Microsoft)