Cet article a fait l'objet d'une traduction automatique.

Facteur DirectX

Construction d'oscillateurs audio pour Windows 8

Charles Petzold

Télécharger l'exemple de code

Charles PetzoldJ'ai été faire des instruments de musique électronique comme un hobby depuis environ 35 ans.J'ai commencé dans le câblage de fin des années 1970 jusqu'à puces CMOS et TTL et beaucoup plus tard a continué la route de logiciel — tout d'abord avec les Extensions multimédia pour Windows en 1991 et, plus récemment, avec la bibliothèque NAudio pour Windows Presentation Foundation (WPF) et la classe MediaStreamSource dans Silverlight et Windows Phone 7.L'année dernière, j'ai consacré quelques tranches de mon Touch & Allez colonne aux applications pour Windows Phone qui jouent le son et la musique.

Je serais probablement blasés par ce temps et peut-être réticents à explorer une autre génération son API.Mais je ne suis pas, parce que je pense que Windows 8 est probablement la meilleure plate-forme de Windows encore pour la fabrication d'instruments de musique.Windows 8 combine une API audio de haute performance — la composante XAudio2 de DirectX — avec des écrans tactiles sur des tablettes de poche.Cette combinaison offre beaucoup de potentiel, et je suis particulièrement intéressé à explorer comment touch peut être exploitée comme une interface subtile et intime pour un instrument de musique entièrement dans le logiciel.

Oscillateurs, Exemples et la fréquence

Au cœur de l'installation de génération sonore de n'importe quel synthétiseur de musique sont des oscillateurs multiples, ainsi appelés parce qu'ils génèrent une onde oscillante plus ou moins périodique à une fréquence particulière et le volume.Dans la génération de sons pour la musique, les oscillateurs qui créent des formes d'onde périodiques invariables habituellement son plutôt ennuyeuses.Oscillateurs plus intéressants incorporent vibrato, tremolo ou changeant de timbres, et ils sont seulement environ périodiques.

Un programme qui désire créer des oscillateurs à l'aide de XAudio2 commence par appeler la fonction XAudio2Create.Ceci fournit un objet qui implémente l'interface IXAudio2.De cet objet, vous pouvez appeler CreateMasteringVoice une seule fois pour obtenir une instance de IXAudio2MasteringVoice, qui fonctionne comme le mélangeur audio principal.Seul IXAudio2MasteringVoice existe à tout moment.En revanche, vous aurez généralement appeler CreateSourceVoice plusieurs fois pour créer plusieurs instances de l'interface IXAudio2SourceVoice.Chacune de ces instances de IXAudio2SourceVoice peut fonctionner comme un oscillateur indépendant.Combiner plusieurs oscillateurs pour un instrument multiphonique, un ensemble ou un orchestre complet.

Un objet IXAudio2SourceVoice génère son en créant et en envoyant des tampons contenant une séquence de nombres qui décrivent une forme d'onde.Ces chiffres sont souvent appelés des échantillons.Ils sont souvent 16 bits de large (la norme pour les CD audio) et ils viennent à un rythme constant — habituellement 44 100 Hz (également la norme CD audio) ou à peu près.Cette technique porte le nom de fantaisie Pulse Code Modulation, ou PCM.

Bien que cette séquence d'échantillons peut décrire une forme d'onde très complexe, souvent un synthétiseur génère un flux relativement simple d'échantillons — plus couramment une onde carrée, une onde triangulaire ou une dent de scie, avec une périodicité correspondant à la fréquence de l'onde (perçue comme de la poix) et une amplitude moyenne qui est perçue comme le volume.

Par exemple, si la fréquence d'échantillonnage est de 44 100 Hz et que chaque cycle de 100 échantillons a des valeurs qui deviennent progressivement plus grand, puis plus petits, puis négatif et retour à zéro, la fréquence du son qui en résulte est de 44 100 divisé par 100, soit 441 Hz — une fréquence proche du centre de perception de la gamme audible pour l'homme.(Une fréquence de 440 Hz est la A au-dessus du milieu C et est utilisée comme un accordage standard.)

L'interface IXAudio2SourceVoice hérite d'une méthode nommée SetVolume de IXAudio2Voice et définit une méthode de sa propre nommée SetFrequencyRatio.J'ai été particulièrement intrigué par cette dernière méthode, car elle semblait constituer un moyen de créer un oscillateur qui génère une forme d'onde périodique particulier à une fréquence variable avec un minimum de tracas.

Figure 1 montre la majeure partie d'une classe nommée SawtoothOscillator1 qui implémente cette technique.Bien que j'utilise des échantillons de familier entier de 16 bits permettant de définir la forme d'onde, XAudio2 utilise en interne les échantillons de point flottant 32 bits.Pour des applications essentielles à la performances, vous aurez probablement envie d'explorer les différences de performances entre entiers et à virgule flottante.

La figure 1 de la classe SawtoothOscillator1

SawtoothOscillator1::SawtoothOscillator1(IXAudio2* pXAudio2)
{
  // Create a source voice
  WAVEFORMATEX waveFormat;
  waveFormat.wFormatTag = WAVE_FORMAT_PCM;
  waveFormat.
nChannels = 1;
  waveFormat.
nSamplesPerSec = 44100;
  waveFormat.
nAvgBytesPerSec = 44100 * 2;
  waveFormat.
nBlockAlign = 2;
  waveFormat.wBitsPerSample = 16;
  waveFormat.cbSize = 0;
  HRESULT hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat,
                                           0, XAUDIO2_MAX_FREQ_RATIO);
  if (FAILED(hr))
    throw ref new COMException(hr, "CreateSourceVoice failure");
  // Initialize the waveform buffer
  for (int sample = 0; sample < BUFFER_LENGTH; sample++)
    waveformBuffer[sample] =
      (short)(65535 * sample / BUFFER_LENGTH - 32768);
  // Submit the waveform buffer
  XAUDIO2_BUFFER buffer = {0};
  buffer.AudioBytes = 2 * BUFFER_LENGTH;
  buffer.pAudioData = (byte *)waveformBuffer;
  buffer.Flags = XAUDIO2_END_OF_STREAM;
  buffer.PlayBegin = 0;
  buffer.PlayLength = BUFFER_LENGTH;
  buffer.LoopBegin = 0;
  buffer.LoopLength = BUFFER_LENGTH;
  buffer.LoopCount = XAUDIO2_LOOP_INFINITE;
  hr = pSourceVoice->SubmitSourceBuffer(&buffer);
  if (FAILED(hr))
    throw ref new COMException(hr, "SubmitSourceBuffer failure");
  // Start the voice playing
  pSourceVoice->Start();
}
void SawtoothOscillator1::SetFrequency(float freq)
{
  pSourceVoice->SetFrequencyRatio(freq / BASE_FREQ);
}
void SawtoothOscillator1::SetAmplitude(float amp)
{
  pSourceVoice->SetVolume(amp);
}

Dans le fichier d'en-tête, une fréquence de base est définie qui divise proprement le taux d'échantillonnage de 44 100. De cela, une taille de mémoire tampon peut être calculée qui est la longueur d'un cycle unique d'une forme d'onde de cette fréquence :

static const int BASE_FREQ = 441;
static const int BUFFER_LENGTH = (44100 / BASE_FREQ);

Dans l'en-tête de fichier est également la définition de cette mémoire tampon comme un champ :

short waveformBuffer[BUFFER_LENGTH];

Après avoir créé l'objet de la IXAudio2SourceVoice, la dent de scie­Oscillator1 constructeur se remplit un tampon avec un cycle d'une forme d'onde en dents de scie, une forme d'onde simple qui va d'une amplitude de-32 768 à une amplitude de 32 767. Ce tampon est soumis à l'IXAudio2SourceVoice avec les instructions qu'il doit être répété pour toujours.

Sans tout autre code, c'est un oscillateur qui joue une onde en dents de scie de 441 Hz pour toujours. C'est très bien, mais il n'est pas très polyvalent. Pour donner un peu plus de polyvalence à SawtoothOscillator1, j'ai également inclus une méthode SetFrequency. L'argument de c'est une fréquence qui la classe utilise pour appeler SetFrequencyRatio. La valeur passée à SetFrequencyRatio peut varier de valeurs float de XAUDIO2_MIN_FREQ_RATIO (ou 1/1,024.0) jusqu'à une valeur maximale spécifiée plus tôt comme un argument de CreateSourceVoice. J'ai utilisé XAUDIO2_MAX_FREQ_RATIO (ou 1,024.0) pour cet argument. La gamme de l'audition humaine — environ 20 Hz à 20 000 Hz — est bien dans les limites définies par ces deux constantes appliquées à la fréquence de base de 441.

Tampons et rappels

Je dois avouer que j'étais au début un peu sceptique quant à la méthode SetFrequencyRatio. Numériquement augmentation et la diminution de la fréquence d'une onde ne sont pas une tâche triviale. Je me suis senti obligé de comparer les résultats avec une forme d'onde générée algorithmiquement. Il s'agit de l'élan derrière le projet OscillatorCompare, qui est le code pour cette rubrique.

Le projet OscillatorCompare inclut la classe SawtoothOscillator1, que j'ai déjà décrit ainsi qu'une classe de SawtoothOscillator2. Cette seconde classe a une méthode SetFrequency qui contrôle la manière dont la classe génère dynamiquement les échantillons qui définissent la forme d'onde. Cette forme d'onde est continuellement construit dans une mémoire tampon et soumis en temps réel à l'objet IXAudio2SourceVoice en réponse aux rappels.

Une classe peut recevoir des rappels de IXAudio2SourceVoice en implémentant l'interface IXAudio2VoiceCallback. Une instance de la classe qui implémente cette interface est ensuite passée comme argument à la méthode CreateSourceVoice. La classe SawtoothOscillator2 implémente cette interface proprement dite et il passe sa propre instance de CreateSourceVoice, en indiquant qu'il ne se faisant usage de SetFrequencyRatio :

pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat,
        XAUDIO2_VOICE_NOPITCH, 1.0f,
        this);

Une classe qui implémente IXAudio2VoiceCallback pouvez utiliser la méthode OnBufferStart pour être averti lorsqu'il est temps de présenter une nouvelle mémoire tampon de données de forme d'onde. Généralement lorsque vous utilisez OnBufferStart pour garder les données à jour, vous aurez envie de conserver une paire de tampons et leur remplaçant. C'est probablement la meilleure solution si vous vous procuriez les données audio d'une autre source, telle qu'un fichier audio. L'objectif est de ne pas laisser le processeur audio deviennent « affamé ». Garder un tampon avant le traitement permet d'éviter la famine, mais ne garantit pas à elle.

Mais j'ai évolué vers une autre méthode définie par IXAudio2VoiceCallback — OnVoiceProcessingPassStart. Sauf si vous travaillez avec des tampons très petites, généralement OnVoiceProcessingPassStart est appelé plus fréquemment que les OnBufferStart et indique quand un segment de données audio est sur le point d'être traitées et combien d'octets est nécessaires. Dans la documentation XAudio2, cette méthode de rappel est présentée comme celui qui a la plus faible latence, qui est souvent très souhaitable pour instruments de musique électronique interactive. Vous ne voulez pas un délai entre la pression sur une touche et d'entendre la note !

Le fichier d'en-tête SawtoothOscillator2 définit deux constantes :

static const int BUFFER_LENGTH = 1024;
static const int WAVEFORM_LENGTH = 8192;

La première constante est la longueur de la mémoire tampon utilisée pour soumettre les données de forme d'onde. Ici, il fonctionne comme un tampon circulaire. Les appels à la méthode OnVoiceProcessingPassStart demandent un certain nombre d'octets. La méthode répond en mettant ces octets dans le tampon (à partir de là où il a laissé la dernière fois) et en appelant SubmitSourceBuffer juste pour ce segment mise à jour de la mémoire tampon. Vous voulez ce tampon d'être suffisamment grand pour votre code de programme n'est pas écraser la partie de la mémoire tampon encore jouée en arrière-plan.

Il s'avère que pour une voix avec un taux d'échantillonnage de 44 100 Hz, les appels à OnVoiceProcessingPassStart demandent toujours 882 octets, soit 441 échantillons de 16 bits. En d'autres termes, le OnVoiceProcessingPassStart est appelé à la vitesse constante de 100 fois par seconde, ou toutes les 10 ms. Bien que non documentée, cette durée 10 ms peut être considérée comme un traitement audio XAudio2 "quantum", et c'est un bon chiffre de garder à l'esprit. Par conséquent, le code que vous écrivez pour que cette méthode ne peut pas traîner. Évitez les appels d'API et des appels à la bibliothèque runtime.

La deuxième constante est la longueur d'un cycle unique de la forme d'onde souhaitée. Il pourrait être la taille d'un tableau contenant les exemples de cette forme d'onde, mais dans SawtoothOscillator2, il est utilisé uniquement pour les calculs.

La méthode SetFrequency dans SawtoothOscillator2 utilise un constant pour calculer un incrément d'angle qui est proportionnel à la fréquence de l'onde :

angleIncrement = (int)(65536.0
                * WAVEFORM_LENGTH
                * freq / 44100.0);

Bien qu'angleIncrement est un entier, elle est traitée comme si elle compose des mots intégraux et fractionnaires. C'est la valeur utilisée pour déterminer chaque échantillon successifs de l'onde.

Par exemple, supposons que l'argument SetFrequency est de 440 Hz. L'angleIncrement est égale à 5 356 535. En hexadécimal, c'est 0x51BBF7, qui est traité comme un entier de 0 x 51 (ou 81 décimal), avec une partie fractionnaire de 0xBBF7, équivalent de 0,734. Si le cycle complet d'une forme d'onde est 8 192 octets et que vous utilisez uniquement la partie entière et sautez 81 octets pour chaque échantillon, la fréquence résultante est 436.05 Hz environ. (Voilà 81 44 100 fois divisé par 8 192). Si vous sautez 82 octets, la fréquence résultante est 441.43 Hz. Vous voulez quelque chose entre ces deux fréquences.

C'est pourquoi une partie fractionnaire doit également entrer dans le calcul. Tout cela serait probablement plus facile en virgule flottante et à virgule flottante peut être encore plus rapide sur certains processeurs modernes, mais Figure 2 montre une approche plus « traditionnels », entier uniquement. Notez que seule la section mise à jour de la mémoire tampon circulaire est spécifiée avec chaque appel à SubmitSourceBuffer.

La figure 2 OnVoiceProcessingPassStart en SawtoothOscillator2

void _stdcall SawtoothOscillator2::OnVoiceProcessingPassStart(UINT32 bytesRequired)
{
  if (bytesRequired == 0)
      return;
  int startIndex = index;
  int endIndex = startIndex + bytesRequired / 2;
  if (endIndex <= BUFFER_LENGTH)
  {
    FillAndSubmit(startIndex, endIndex - startIndex);
  }
  else
  {
    FillAndSubmit(startIndex, BUFFER_LENGTH - startIndex);
    FillAndSubmit(0, endIndex % BUFFER_LENGTH);
  }
  index = (index + bytesRequired / 2) % BUFFER_LENGTH;
}
void SawtoothOscillator2::FillAndSubmit(int startIndex, int count)
{
  for (int i = startIndex; i < startIndex + count; i++)
  {
    pWaveformBuffer[i] = (short)(angle / WAVEFORM_LENGTH - 32768);
    angle = (angle + angleIncrement) % (WAVEFORM_LENGTH * 65536);
  }
  XAUDIO2_BUFFER buffer = {0};
  buffer.AudioBytes = 2 * BUFFER_LENGTH;
  buffer.pAudioData = (byte *)pWaveformBuffer;
  buffer.Flags = 0;
  buffer.PlayBegin = startIndex;
  buffer.PlayLength = count;
  HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer);
  if (FAILED(hr))
    throw ref new COMException(hr, "SubmitSourceBuffer");
}

SawtoothOscillator1 et SawtoothOscillator2 peuvent être comparé-by-side dans le programme OscillatorCompare. MainPage possède deux paires de curseurs pour modifier la fréquence et le volume de chaque oscillateur. Le contrôle Slider pour la fréquence génère uniquement des valeurs de nombre entier allant de 24 à 132. J'ai emprunté ces valeurs à partir des codes utilisés dans le MIDI Musical Instrument Digital Interface () standard pour représenter des emplacements. La valeur de 24 correspond aux C trois octaves inférieures middle-C, qui s'appelle C 1 C à octave 1 en notation scientifique de terrain et a une fréquence d'environ 32,7 Hz. La valeur de 132 correspond à une fréquence d'environ 16 744 Hz C 10 et six octaves au-dessus du milieu-C. Un convertisseur d'info-bulle sur ces curseurs affiche la valeur actuelle en notation scientifique de terrain et la fréquence équivalente.

J'ai expérimenté avec ces deux oscillateurs, je ne pouvais pas entendre une différence. J'ai également installé un oscilloscope du logiciel sur un autre ordinateur pour examiner visuellement les formes d'onde résultantes, et je ne pouvais pas voir de différence non plus. Cela m'indique que le SetFrequency­Ratio méthode est implémentée de façon intelligente, qui bien sûr nous devons nous attendre à un système aussi sophistiqués que DirectX. Je soupçonne que les interpolations sont effectuées sur les données de forme d'onde rééchantillonnée à changer la fréquence. Si vous êtes nerveux, vous pouvez définir la BASE_FREQ très faible — par exemple, jusqu'à 20 Hz — et la classe va générer une forme d'onde détaillé composé de 2 205 échantillons. Vous pouvez également expérimenter avec une grande valeur : Par exemple, 8 820 Hz provoquera une ondulation de seulement cinq échantillons à générer ! Certes, cela a un son un peu différent parce que la forme d'onde interpolée se situe quelque part entre une dent de scie et une onde triangulaire, mais la forme d'onde résultante est toujours lisse sans « effets d'escalier ».

Cela ne veut ne pas dire que tout fonctionne hunky dory. Avec un oscillateur en dents de scie, les octaves haut couple obtenir assez chaotiques. L'échantillonnage de l'onde tend à émettre de hautes et basses fréquences harmoniques d'une sorte j'ai entendu avant, et dont j'ai l'intention d'étudier plus en détail dans l'avenir.

Gardez le Volume !

La méthode SetVolume définie par IXAudio2Voice et hérité par IXAudio2SourceVoice est documentée comme un multiplicateur à virgule flottante qui peut être défini à des valeurs comprises entre -2 ^ 24 à 2 ^ 24, ce qui équivaut à 16 777 216.

Toutefois, dans la vraie vie, vous aurez probablement envie de garder le volume sur un objet IXAudio2SourceVoice à une valeur comprise entre 0 et 1. 0 Valeur correspond au silence et 1 correspondant à aucun gain ou l'atténuation. Gardez à l'esprit que quelle que soit la source de l'onde associée une IXAudio2SourceVoice — si elle est algorithmiquement généré ou provient d'un fichier audio, il a sans doute des échantillons de 16 bits qui viennent très probablement proches des valeurs minimales et maximales de-32 768 et 32 767. Si vous essayez d'amplifier ces formes d'onde avec un niveau de volume supérieur à 1, les échantillons dépasseront la largeur d'un entier de 16 bits et seront découpés aux valeurs minimales et maximales. Distorsion et bruit entraînera.

Cela devient critique lorsque vous démarrez combinant plusieurs instances de IXAudio2SourceVoice. Les formes d'onde de ces instances multiples sont mélangés par étant additionnées. Si vous permettez à chacun de ces cas d'avoir un volume de 1, la somme des voix pourrait très bien les échantillons qui dépassent la taille des entiers 16 bits. Cela peut se produire sporadiquement — entraînant seulement intermittente distorsion — ou chroniquement, résultant en une véritable pagaille.

Lorsque vous utilisez plusieurs instances de IXAudio2SourceVoice qui génèrent des formes d'onde pleine 16-bits de large, une mesure de sécurité est régler le volume de chaque oscillateur 1 divisé par le nombre de voix. Qui garantit que la somme ne dépasse jamais une valeur de 16 bits. Un réglage de volume global peut également via la voix de mastering. Vous pouvez également se pencher sur la fonction XAudio2CreateVolumeMeter, qui vous permet de créer un objet de traitement audio qui peut aider le volume du moniteur pour des fins de débogage.

Notre premier Instrument de musique

Il est commun pour des instruments de musique sur des tablettes d'avoir un clavier de type piano, mais j'ai été intrigué récemment par un type de clavier bouton trouvé sur accordéons tels que le russe bayan (dont j'ai connaissance de le œuvre du compositeur russe Sofia Gubaidulina). Parce que chaque touche est une touche plutôt que d'un levier long, beaucoup plus de clés peuvent être emballés au sein de l'espace limité de l'écran de la tablette, comme le montre Figure 3.

The ChromaticButtonKeyboard Program
Figure 3 le programme de ChromaticButtonKeyboard

Les deux lignes de fond dupliquer les clés sur les deux premières lignes et sont fournis pour faciliter le doigté des accords communs et des séquences mélodiques. Dans le cas contraire, chaque groupe de 12 touches dans les top trois lignes fournissent toutes les notes de l'octave, généralement croissant de gauche à droite. L'étendue totale ici quatre octaves, qui est sur deux fois ce que vous obtiendriez avec un clavier de piano de la même taille.

Un véritable bayan a une octave supplémentaire, mais je ne pouvais pas m'intégrer il sans faire les boutons trop petits. Le code source permet de définir des constantes pour essayer cette octave supplémentaire, ou pour éliminer une autre octave et agrandir les boutons.

Parce que je ne peux pas prétendre que ce programme ressemble à n'importe quel instrument qui existe dans le monde réel, je l'ai simplement appelé ChromaticButton­clavier. Les clés sont des instances d'un contrôle personnalisé nommé clé qui dérive de ContentControl, mais effectue des traitements touch pour maintenir une propriété IsPressed et générer un événement IsPressedChanged. La différence entre le toucher manipulation dans ce contrôle et au toucher de manutention dans un bouton ordinaire (qui possède également une propriété IsPressed) est visible lorsque vous balayez votre doigt sur le clavier : Un bouton standard définira la propriété IsPressed true uniquement si la presse doigt se produit sur la surface du bouton, alors que ce contrôle personnalisé de la clé considère la touche à presser si un doigt balaie à partir du côté.

Le programme crée six instances d'une classe SawtoothOscillator qui est pratiquement identique à la classe SawtoothOscillator1 du projet antérieur. Si votre écran tactile le permet, vous pouvez jouer six notes simultanées. Il n'y a pas de rappels et de la fréquence de l'oscillateur est contrôlée par les appels à la méthode SetFrequencyRatio.

Pour garder une trace de quels oscillateurs sont disponibles et quels oscillateurs jouent, le fichier MainPage.xaml.h définit deux objets de collection standard sous forme de champs :

std::vector<SawtoothOscillator *> availableOscillators;
std::map<int, SawtoothOscillator *> playingOscillators;

A l'origine, chaque objet de clé a sa propriété Tag défini sur le code de note MIDI que j'ai discuté plus tôt. Voilà comment le gestionnaire de IsPressedChanged détermine quelle touche a été enfoncé et quelle fréquence pour calculer. Ce code MIDI servit également de la carte-clé pour la collection playingOscillators. Il a bien fonctionné jusqu'à ce que j'ai joué une note depuis les deux lignes de fond qui reproduit une note déjà jouer, qui a abouti à une clé en double et d'une exception. Facilement, j'ai résolu ce problème en intégrant une valeur dans la propriété Tag indiquant que la ligne où se trouve la clé : La balise est égal maintenant le code de note MIDI plus de 1000 fois le nombre de ligne.

Figure 4 illustre le gestionnaire de IsPressedChanged pour les instances de la clé. Lorsqu'une touche est enfoncée, un oscillateur est supprimé de la collection availableOscillators, donné à une fréquence et un volume différent de zéro et inséré dans la collection playingOscillators. Lorsqu'une touche est relâchée, l'oscillateur est donné un volume nul et revint à availableOscillators.

Figure 4 le gestionnaire de IsPressedChanged pour les Instances de la clé

void MainPage::OnKeyIsPressedChanged(Object^ sender, bool isPressed)
{
  Key^ key = dynamic_cast<Key^>(sender);
  int keyNum = (int)key->Tag;
  if (isPressed)
  {
    if (availableOscillators.size() > 0)
    {
      SawtoothOscillator* pOscillator = availableOscillators.back();
      availableOscillators.pop_back();
      double freq = 440 * pow(2, (keyNum % 1000 - 69) / 12.0);
      pOscillator->SetFrequency((float)freq);
      pOscillator->SetAmplitude(1.0f / NUM_OSCILLATORS);
      playingOscillators[keyNum] = pOscillator;
    }
  }
  else
  {
    SawtoothOscillator * pOscillator = playingOscillators[keyNum];
    if (pOscillator != nullptr)
    {
      pOscillator->SetAmplitude(0);
      availableOscillators.push_back(pOscillator);
      playingOscillators.erase(keyNum);
    }
  }
}

C'est environ aussi simple comme un instrument multi-voix peut être, et bien sûr il est erronée : Sons ne doivent pas être activés en marche comme un interrupteur. Le volume devrait glisser vers le haut rapidement mais en douceur quand une note commence et repli quand il s'arrête. Beaucoup de vrais instruments ont aussi un changement dans le volume et le timbre en cours de note. Il y a encore beaucoup de place pour des améliorations.

Mais compte tenu de la simplicité du code, il fonctionne étonnamment bien et est très sensible. Si vous compilez le programme pour les processeurs ARM, vous pouvez les déployer sur le bras-a basé la Surface de Microsoft et de marcher berçant la tablette non attachée à un bras en jouant à ce sujet avec l'autre main, je dois dire est un peu un frisson.

Charles Petzold est un contributeur de longue date à MSDN Magazine et l'auteur de « Programmation Windows, 6e édition » (o ' Reilly Media, 2012), un livre sur l'écriture d'applications pour Windows 8. Son site Web est charlespetzold.com.

Merci aux experts techniques suivants d'avoir relu cet article : Tom Mathews et Thomas Petchel