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

Facteur DirectX

Simulation de synthétiseur analogique

Charles Petzold

Télécharger l'exemple de code

Charles Petzold
Environ 50 ans, un physicien et ingénieur nommé Robert Moog créé un synthétiseur de musique électronique avec une fonctionnalité plutôt inhabituelle : un clavier orgue.Certains compositeurs de musique électronique décrié ce dispositif contrôle prosaïque et surannée, tandis que d'autres compositeurs — et en particulier les interprètes, s'est félicité de cette évolution.La fin des années 1960, Wendy Carlos Switched-on Bach était devenu un des albums classiques plus vendus de tous les temps, et le synthétiseur Moog est entré dans le courant dominant.

Les premiers synthétiseurs Moog étaient modulaires et programmé avec des câbles de raccordement.En 1970, cependant, est diffusé sur le Minimoog — au prix de seulement 1 495 $, petit et facile à utiliser et à jouer.(Une bonne histoire de ces premiers synthétiseurs est le livre, "jours analogiques : L'Invention et l'Impact du synthétiseur Moog"[Harvard University Press, 2004], par Trevor Pinch et Frank Trocco.)

Nous classons les Moog et des synthétiseurs analogues comme dispositifs « analogiques » parce qu'elles créent des sons à l'aide de différentes tensions générées à partir d'un circuit construit à partir de transistors, résistances et condensateurs.En revanche, plus modernes synthétiseurs « numériques » créent des sons par le biais de calculs algorithmiques ou échantillons numérisés.Les appareils plus anciens sont en outre classés comme « soustractives » synthétiseurs : Plutôt que de construire un son composite au moyen de la combinaison d'ondes sinusoïdales (une technique appelée synthèse additive), synthétiseur soustractif commence par une forme d'onde est riche en harmoniques — comme une onde en dents de scie ou carrée — et puis exécutez-le à travers des filtres pour éliminer certaines harmoniques et en modifier le timbre du son.

Un concept crucial mis au point par Robert Moog a été « contrôle de la tension ». Considérons un oscillateur, qui est le composant d'un synthétiseur qui génère une forme d'onde audio de base quelconque.Dans les synthétiseurs antérieures, la fréquence de cette onde pourrait être contrôlée par la valeur d'une résistance quelque part dans le circuit, et cette résistance variable pourrait être contrôlée par un cadran.Mais dans un oscillateur commandé en tension (VCO), la fréquence de l'oscillateur est régie par une tension d'entrée.Par exemple, chaque augmentation d'un volt à l'oscillateur peut doubler les fréquences de l'oscillateur.De cette manière, la fréquence du VCO peut être contrôlée par un clavier qui génère une tension qui augmente d'un volt par octave.

Dans les synthétiseurs analogiques, la sortie d'un ou plusieurs VCO va dans un filtre commandé en tension (VCF) permettant de modifier le contenu harmonique de la forme d'onde.Tensions d'entrée pour le FCV contrôlent la fréquence de coupure du filtre, ou la netteté de la réponse du filtre (du filtre de qualité, ou Q).La sortie de la VCF passe ensuite dans un amplificateur commandé en tension (VCA), le gain qui est contrôlé par une autre tension.

Générateurs d'enveloppe

Mais une fois que vous commencez à parler de FCR et OCV, les choses se compliquent, et un peu d'histoire est nécessaire.

Le 19ème siècle, certains scientifiques (notamment Hermann von Helmholtz) a commencé à faire des percées significatives dans l'exploration de la physique et la perception du son.Caractéristiques du son tels que la fréquence et le volume s'est avéré d'être relativement simple par rapport au problème épineux du timbre — que la qualité sonore qui permet de distinguer un piano d'un violon ou un trombone.Il a été émis l'hypothèse (et un peu montré) que le timbre était lié à contenu harmonique du son, c'est-à-dire les divers degrés d'intensité des sinusoïdes qui constituent le son.

Mais au début du XXe siècle chercheurs étudie plus loin, ils ont découvert que ce n'était pas aussi simple.Contenu harmonique change au cours d'une tonalité musicale, et cela contribue à timbre de l'instrument.En particulier, le début d'une note d'un instrument de musique est essentiels à la perception auditive.Lorsqu'un arc marteau ou violon piano touche tout d'abord une chaîne, ou vibrant air est propulsé dans un tube en métal ou en bois, activité harmonique très complexe se produit.Cette complexité diminue très rapidement, mais sans elle, les tonalités musicales bruit sourd et beaucoup moins intéressant et distinctif.

Pour imiter la complexité des tonalités musicales réel, un synthétiseur ne peut pas placer simplement une note sur et en dehors, comme un interrupteur.(Pour entendre ce qui tel un synthétiseur simple ressemble à, vérifiez le programme de ChromaticButtonKeyboard dans l'article de février 2013 de cette colonne à msdn.microsoft.com/magazine/jj891059.) Au début de chaque note, le son doit avoir un bref « blip » de grand volume et le timbre différentes avant de se stabiliser.Lorsque la note se termine, le son ne doit pas tout simplement arrêter, mais mourir dehors avec une diminution en volume et en complexité.

Pour volume, il y a une tendance générale à ce processus : Pour une note jouée sur un instrument de la chaîne, en laiton ou bois, le son s'élève à un niveau sonore maximum rapidement, puis meurt au large un peu et bloque.Lorsque la note se termine, il diminue rapidement en volume.Ces deux phases sont connus comme le « attaque » et de « release ».

Pour les instruments de percussion plus — y compris le piano — la note atteint le volume maximum rapidement lors de l'attaque mais meurt puis lentement si l'instrument reste non amortie, par exemple, tout en maintenant la touche enfoncée.Une fois que la touche est relâchée, la note meurt rapidement.

Pour obtenir ces effets, synthétiseurs de mettre en oeuvre ce qu'on appelle un « générateur d'enveloppe ». Figure 1 montre de manière assez standard appelée une enveloppe attack-carie-soutenir-release (ADSR).L'axe horizontal est temps, et l'axe vertical est loudness.

An Attack-Decay-Sustain-Release Envelope
Figure 1 une enveloppe Attack Decay-soutenir-libération

Quand une touche sur un clavier et la note commence à sonner, vous entendez les sections attaque et désintégration qui donnent un éclat sonore tout d'abord, et puis la note se stabilise au niveau sustain.Quand la touche est relâchée et les extrémités de la note, la section de sortie se produit.Pour un son de type piano, le temps de décroissance pourrait être de quelques secondes, et le niveau de soutien est mis à zéro pour le son continue à chuter aussi longtemps que la touche est maintenue enfoncée.

Même les plus simples synthétiseurs analogiques ont deux enveloppes ADSR : On contrôle le volume et l'autre contrôle le filtre.Il s'agit généralement d'un filtre passe-bas.Comme une note est frappée, la fréquence de coupure est augmentée rapidement pour permettre de plus hautes fréquences harmoniques à travers, et ensuite la fréquence de coupure diminue un peu.A souligné beaucoup, cela crée le synthétiseur analogique distinctif chant sonore.

Le projet AnalogSynth

Environ neuf mois il y a, comme je contemplais en utilisant XAudio2 pour programmer une simulation numérique d'un synthétiseur analogique de la petite ère des années 70, j'ai réalisé que les générateurs d'enveloppe serait l'un des aspects plus difficiles du travail.Ce n'était pas encore clair pour moi si ces générateurs d'enveloppe seraient externe dans le flux de traitement audio (et donc accéder aux méthodes SetVolume et SetFilterParameters d'une voix XAudio2), ou en quelque sorte être intégré dans le flux audio.

Je me suis finalement installé sur l'implémentation des enveloppes comme XAudio2 effets audio — plus formellement connu en tant qu'objets de traitement Audio (APOs).Autrement dit, que la logique de l'enveloppe fonctionne directement sur le flux audio.Je suis devenu plus confiant avec cette approche après que codage logique du filtre qui fait double emploi avec les filtres biquad numérique intégré à XAudio2.En utilisant mon propre code filtre, j'ai pensé que je pourrais être capable de modifier l'algorithme de filtrage à l'avenir sans perturbations majeures de la structure du programme.

La figure 2 montre l'écran de l'analogique qui en résulte­programme de synthé, dont la source code que vous pouvez télécharger à archive.msdn.microsoft.com/mag201307DXF.Bien que j'ai été influencé par la disposition des contrôles sur le Minimoog, j'ai gardé l'interface utilisateur réel assez simple, en utilisant, par exemple, les curseurs plutôt que des cadrans.La plupart de mon accent était sur les composants internes.

The AnalogSynth Screen
Figure 2 l'écran AnalogSynth

Le clavier est une série de contrôles personnalisés de clé de traitement des événements de pointeur et regroupés en contrôles d'Octave.Le clavier est en fait de six octaves en largeur et peut faire défiler horizontalement à l'aide de la bande grise épaisse sous les touches.Un point rouge identifie le milieu c.

Le programme peut jouer 10 notes simultanées, mais qui est modifiable avec un simple #define dans MainPage.xaml.cs.(Les premiers synthétiseurs analogiques comme le Minimoog étaient monophoniques). Chacune des ces 10 voix est une instance d'une classe, j'ai appelé SynthVoice.SynthVoice a des méthodes pour définir tous les paramètres différents de la voix (y compris les enveloppes, le volume et la fréquence), ainsi que de méthodes nommées détente et relâchez-la pour indiquer quand une clé a été enfoncée ou relâchée.

Le Minimoog atteint sa sonorité caractéristique « punchy » en partie par la présence de deux oscillateurs en cours d'exécution en parallèle et souvent légèrement mistuned, soit intentionnelle ou due à la fréquence dérive courant dans un circuit analogique.

Pour cette raison, chaque SynthVoice crée deux instances d'une classe de l'oscillateur, qui sont contrôlés par le haut à gauche du panneau de commande indiqué dans Figure 2.Le panneau de contrôle vous permet de définir le signal et le volume relatif de ces deux oscillateurs, et vous pouvez transposer la fréquence par une ou deux octaves vers le haut ou vers le bas.En outre, vous pouvez décaler la fréquence de l'oscillateur deuxième à jusqu'à une demi-octave.

Chaque instance d'oscillateur crée un objet IXAudio2SourceVoice et expose les méthodes nommées SetFrequency, SetAmplitude et SetWaveform.Achemine les deux sorties de IXAudio2SourceVoice à une IXAudio2SubmixVoice SynthVoice et puis instancie deux effets audio personnalisés appelés FilterEnvelopeEffect et Amplitude­EnvelopeEffect qu'il apporte à cette voix de mixage secondaire.Ces deux effets partagent une classe appelée EnvelopeGenerator que je décrirai bientôt.

Figure 3 montre l'Organisation des composants dans chaque SynthVoice.Pour les 10 objets de SynthVoice, il y a un total de 20 IXAudio2Source­voix instances entrer dans 10 cas de IXAudio2SubmixVoice, qui sont ensuite acheminés vers un seul IXAudio2MasteringVoice.J'utilise une fréquence d'échantillonnage de 48 000 Hz et 32 bits à virgule flottante échantillons tout au long.

The Structure of the SynthVoice Class
Figure 3 la Structure de la classe SynthVoice

L'utilisateur contrôle le filtre de la section centrale du panneau de contrôle.Un contrôle ToggleButton permet au filtre d'être contournés ; dans le cas contraire, la fréquence de coupure est par rapport à la note qui est en cours de lecture.(En d'autres termes, la fréquence de coupure du filtre suit le clavier.) La Emph­asis curseur contrôle réglage Q du filtre.Le curseur de l'enveloppe contrôle le degré auquel l'enveloppe affecte la fréquence de coupure du filtre.

Les quatre curseurs associées à l'enveloppe de filtre et l'enveloppe de volume fonctionnent de manière similaire.Les curseurs Attack, Decay et Release sont toutes les durées de 10 millisecondes à 10 secondes dans une échelle logarithmique.Les curseurs sont des convertisseurs de valeur d'info-bulle pour afficher la durée associée à des paramètres.

AnalogSynth ne fait aucun réglage de volume pour les 20 instances de IXAudio2SourceVoice simultanées des potentiels, ou pour contrecarrer la tendance des filtres biquad numérique à amplifier audio près de la fréquence de coupure.Par conséquent, l'AnalogSynth le rend facile de surcharger l'audio.Pour aider l'utilisateur à éviter cela, le programme utilise le XAudio2­CreateVolumeMeter fonction pour créer un effet audio qui surveille le son sortant.Si le point vert dans le coin supérieur droit passe au rouge, sortie audio est étant adaptée et utilisez le curseur à l'extrême droite diminuer le volume.

Premiers synthétiseurs permettant de connecter des composants cordons de raccordement.À la suite de cet héritage, une configuration particulière de synthétiseur est toujours connue comme un "patch". Si vous trouvez un patch qui émet un son que vous souhaitez conserver, appuyez sur le bouton "sauvegarder" et assignez-lui un nom.Appuyez sur le bouton Load pour obtenir une liste de précédemment enregistré les patchs et choisir un.Ces patchs (ainsi que la configuration actuelle) est stockée dans la zone paramètres locaux.

L'algorithme de générateur d'enveloppe

Le code qui implémente un générateur d'enveloppe est essentiellement une machine à États, avec cinq États séquentiels que j'ai appelé Dormant, Attack, Decay, Sustain et Release.Du point de vue interface utilisateur, il semble plus naturel d'attaque, decay, de spécifier et de maintenir en termes de temps dura­tions, mais quand en fait du calcul vous devez convertir à un taux — une augmentation ou une diminution de volume (ou la fréquence de coupure du filtre) par unité de temps.Les deux effets audio dans AnalogSynth utilisent ces fluctuations du niveau d'appliquer l'effet.

Cette machine de l'État n'est pas toujours aussi séquentielle comme le schéma de Figure 1 semblerait impliquer.Par exemple, que se passe-t-il lorsqu'une touche est enfoncée et sortie si vite que l'enveloppe n'a pas encore atteint la section sustain lorsque la touche est relâchée ?Au début, je pensais l'enveloppe puissent remplir les sections de son attaque et désintégration et puis aller à droite dans la section de sortie, mais cela ne fonctionne pas bien pour une enveloppe de type piano.Dans une enveloppe de piano, le niveau de soutien est de zéro, et le temps de décroissance est relativement long.Une clé rapidement, pressé et relâché avait encore une longue décadence — comme s'il était délivré pas du tout !

J'ai décidé que pour une presse rapide et la libération, je laisserais la section attaque complet, mais ensuite immédiatement accéder à la section de sortie.Cela signifie que le taux final de diminution devrait être calculé basé sur le niveau actuel.Cela explique pourquoi il y a une différence dans la façon dont la libération est gérée dans la structure pour les paramètres d'enveloppe, illustré ici :

struct EnvelopeGeneratorParameters
{
  float baseLevel;
  float attackRate;   // in level/msec
  float peakLevel;
  float decayRate;    // in level/msec
  float sustainLevel;
  float releaseTime;  // in msec
};

Pour l'enveloppe d'amplitude, visant a la valeur 0, peakLevel est défini sur 1 et sustainLevel est quelque part entre ces valeurs. Pour l'enveloppe de filtre, les trois niveaux se référer à un coefficient multiplicateur appliqué à la fréquence de coupure du filtre : Visant est 1, et peakLevel est régie par le curseur marqué « Enveloppe » et peut varier de 1 à 16. Ce multiplicateur de fréquence de 16 correspond à quatre octaves.

Les AmplitudeEnvelopeEffect et FilterEnvelopeEffect appartiennent à la classe de EnvelopeGenerator. Figure 4 affiche le fichier d'en-tête EnvelopeGenerator. Notez la méthode publique pour définir les paramètres d'enveloppe et deux méthodes publiques nommés Attack et Release qui déclenchent l'enveloppe pour commencer et finir. Ces trois méthodes doivent être appelées dans l'ordre. Le code n'est pas écrit pour traiter une enveloppe dont les paramètres changent à mi-chemin de sa progression.

Figure 4 le fichier d'en-tête EnvelopeGenerator

class EnvelopeGenerator
{
private:
  enum class State
  {
    Dormant, Attack, Decay, Sustain, Release
  };
  EnvelopeGeneratorParameters params;
  float level;
  State state;
  bool isReleased;
  float releaseRate;
public:
  EnvelopeGenerator();
  void SetParameters(const EnvelopeGeneratorParameters params);
  void Attack();
  void Release();
  bool GetNextValue(float interval, float& value);
};

La valeur calculée actuelle depuis le générateur d'enveloppe est obtenue grâce à des appels répétés à GetNextValue. L'intervalle argu­ment est en millisecondes et la méthode calcule une valeur nouvelle basée sur cet intervalle, éventuellement Etats dans le processus de commutation. Lorsque l'enveloppe est terminée avec la section de sortie, GetNextValue retourne true pour indiquer que l'enveloppe est terminée, mais je n'utilise en fait cette valeur de retour ailleurs dans le programme.

Figure 5 illustre l'implémentation de la classe EnvelopeGenerator. Haut de la GetNextValue méthode est le code pour sauter directement à l'état de libération lorsqu'une touche est relâchée, et le calcul du taux de rejet est basé sur le niveau actuel et le temps de libération.

Figure 5 la mise en œuvre de EnvelopeGenerator

EnvelopeGenerator::EnvelopeGenerator() : state(State::Dormant)
{
  params.baseLevel = 0;
}
void EnvelopeGenerator::SetParameters(const EnvelopeGeneratorParameters params)
{
  this->params = params;
}
void EnvelopeGenerator::Attack()
{
  state = State::Attack;
  level = params.baseLevel;
  isReleased = false;
}
void EnvelopeGenerator::Release()
{
  isReleased = true;
}
bool EnvelopeGenerator::GetNextValue(float interval, float& value)
{
  bool completed = false;
  // If note is released, go directly to Release state,
  // except if still attacking
  if (isReleased &&
    (state == State::Decay || state == State::Sustain))
  {
    state = State::Release;
    releaseRate = (params.baseLevel - level) / params.releaseTime;
  }
  switch (state)
  {
  case State::Dormant:
    level = params.baseLevel;
    completed = true;
    break;
  case State::Attack:
    level += interval * params.attackRate;
    if ((params.attackRate > 0 && level >= params.peakLevel) ||
      (params.attackRate < 0 && level <= params.peakLevel))
    {
      level = params.peakLevel;
      state = State::Decay;
    }
    break;
  case State::Decay:
    level += interval * params.decayRate;
    if ((params.decayRate > 0 && level >= params.sustainLevel) ||
      (params.decayRate < 0 && level <= params.sustainLevel))
    {
      level = params.sustainLevel;
      state = State::Sustain;
    }
    break;
  case State::Sustain:
    break;
  case State::Release:
    level += interval * releaseRate;
    if ((releaseRate > 0 && level >= params.baseLevel) ||
      (releaseRate < 0 && level <= params.baseLevel))
    {
      level = params.baseLevel;
      state = State::Dormant;
      completed = true;
    }
  }
  value = level;
  return completed;
}

Une paire d'effets Audio

Le AmplitudeEnvelopeEffect et le FilterEnvelopeEffect des classes dérivent de CXAPOParametersBase donc ils peuvent accepter des paramètres, et les deux classes entretiennent également une instance de la classe EnvelopeGenerator pour effectuer les calculs de l'enveloppe. Les structures de paramètre pour ces deux effets audio sont nommés AmplitudeEnvelopeParameters et FilterEnvelopeParameters.

La structure AmplitudeEnvelopeParameters est simplement une structure EnvelopeGeneratorParameters et un champ de keyPressed booléenne qui est true lorsque la touche associée à cette voix est pressé et fausse quand il est libéré. (Le filtre­structure EnvelopeParameters est juste un peu plus complexe car elle doit incorporer une fréquence de coupure du filtre de niveau de base et le paramètre Q.) Les deux classes d'effets maintiennent leurs propres membres de données de keyPressed qui peuvent être comparées à la valeur des paramètres pour déterminer quand l'enveloppe attaquent ou au rejet d'État doit être déclenchée.

Vous pouvez voir comment cela fonctionne Figure 6, qui affiche le code pour la substitution du processus en AmplitudeEnvelopeEffect. Si l'effet est activé et la valeur de keyPressed locale a la valeur false, mais la valeur keyPressed dans les paramètres des effets est true, alors l'effet effectue des appels aux méthodes SetParameters et attaque de l'instance de EnvelopeGenerator. Si c'est exactement le contraire — la valeur locale de keyPressed est vraie, mais celui de paramètres a la valeur false, puis l'effet appelle la méthode Release.

Figure 6 la substitution du processus en AmplitudeEnvelopeEffect

void AmplitudeEnvelopeEffect::Process(UINT32 inpParamCount,
    const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParam,
    UINT32 outParamCount,
    XAPO_PROCESS_BUFFER_PARAMETERS *pOutParam,
    BOOL isEnabled)
{
  // Get effect parameters
  AmplitudeEnvelopeParameters * pParams =
    reinterpret_cast<AmplitudeEnvelopeParameters *>
      (CXAPOParametersBase::BeginProcess());
  // Get buffer pointers and other information
  const float * pSrc = static_cast<float const*>(pInpParam[0].pBuffer);
  float * pDst = static_cast<float *>(pOutParam[0].pBuffer);
  int frameCount = pInpParam[0].ValidFrameCount;
  int numChannels = waveFormat.
nChannels;
  switch(pInpParam[0].BufferFlags)
  {
  case XAPO_BUFFER_VALID:
    if (!isEnabled)
    {
      for (int frame = 0; frame < frameCount; frame++)
      {
        for (int channel = 0; channel < numChannels; channel++)
        {
          int index = numChannels * frame + channel;
          pDst[index] = pSrc[index];
        }
      }
    }
    else
    {
      // Key being pressed
      if (!this->keyPressed && pParams->keyPressed)
      {
        this->keyPressed = true;
        this->envelopeGenerator.SetParameters(pParams->envelopeParams);
        this->envelopeGenerator.Attack();
      }
      // Key being released
      else if (this->keyPressed && !pParams->keyPressed)
      {
        this->keyPressed = false;
        this->envelopeGenerator.Release();
      }
      // Calculate interval in msec
      float interval = 1000.0f / waveFormat.
nSamplesPerSec;
      for (int frame = 0; frame < frameCount; frame++)
      {
        float volume;
        envelopeGenerator.GetNextValue(interval, volume);
        for (int channel = 0; channel < numChannels; channel++)
        {
          int index = numChannels * frame + channel;
          pDst[index] = volume * pSrc[index];
        }
      }
    }
    break;
  case XAPO_BUFFER_SILENT:
    break;
  }
  // Set output parameters
  pOutParam[0].ValidFrameCount = pInpParam[0].ValidFrameCount;
  pOutParam[0].BufferFlags = pInpParam[0].BufferFlags;
  CXAPOParametersBase::EndProcess();
}

L'effet pourrait appeler la méthode GetNextValue de EnvelopeGenerator pour chaque appel de processus (auquel cas l'argument interval indiquerait 10 millisecondes) ou pour tous les échantillons (dans ce cas, l'intervalle est plus comme 21 microsecondes). Bien que la première approche devrait suffire, j'ai décidé sur le second pour les transitions théoriquement plus lisses.

La valeur à virgule flottante volume retourné par l'appel de GetNextValue varie de 0 (quand une note est le premier commençant ou se terminant) à 1 pour le point culminant de l'attaque. L'effet multiplie simplement les échantillons en virgule flottante par ce numéro.

Le plaisir commence maintenant

J'ai passé tellement de temps, l'analogue de codage­programme de synthé qui je n'ai pas eu beaucoup de temps à jouer avec elle. Il pourrait très bien être que certains des contrôles et paramètres besoin mise au point, ou peut-être un peu plus grossière tuning ! En particulier, depuis longtemps, se désintègrent et libération fois sur le volume ne sonnent pas tout à fait raison, et ils suggèrent que les changements de l'enveloppe d'amplitude doivent être logarithmique et non linéaire.

Je suis également intrigué par l'utilisation de la saisie tactile avec le clavier à l'écran. Les touches sur un vrai piano sont sensibles à la vitesse avec laquelle ils sont frappés, et claviers de synthétiseur ont tenté d'imiter cette même sensation. Écrans tactiles la plupart, cependant, ne peut pas détecter touch vitesse ou la pression. Mais ils peuvent être faits sensibles aux mouvements des doigts légèrement sur l'écran, ce qui est au-delà des capacités d'un vrai clavier. À l'écran claviers rendre plus sensibles de cette façon ? Il y a qu'un seul moyen de savoir !

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 à l'expert technique suivant d'avoir relu cet article : James McNellis (Microsoft)
James McNellis est un aficionado de C++ et un développeur de logiciels dans l'équipe Visual C++ chez Microsoft, où il où il construit des bibliothèques C++ et tient à jour les bibliothèques Runtime C (CRT).  Il tweets à @JamesMcNelliset se retrouve ailleurs en ligne via http://jamesmcnellis.com/.