Il presente articolo è stato tradotto automaticamente.

Fattore DirectX

Costruzione di oscillatori audio per Windows 8

Charles Petzold

Scarica il codice di esempio

Charles PetzoldSto facendo musica elettronica strumenti come un hobby per circa 35 anni. Ho iniziato nel tardo cablaggio degli anni settanta su chip TTL e CMOS e molto più tardi è andato via il software — prima con il Multimedia Extensions per Windows nel 1991 e, più recentemente, con la libreria NAudio per Windows Presentation Foundation (WPF) e la classe MediaStreamSource in Silverlight e Windows Phone 7. Proprio l'anno scorso, ho dedicato un paio rate del mio Touch & Vai colonna alle applicazioni per Windows Phone che Riproduci suono e musica.

Probabilmente dovrei essere stanchi di questo tempo e forse riluttante a esplorare un'altra generazione suono API. Ma io non sono, perché penso che Windows 8 è probabilmente la migliore piattaforma di Windows ancora per la fabbricazione di strumenti musicali. Windows 8 combina una API audio ad alte prestazioni — il componente XAudio2 di DirectX — con touchscreen sul palmare compresse. Questa combinazione offre molte potenzialità, e sono particolarmente interessato a esplorare come tocco può essere sfruttata come un'interfaccia intima e sottile di uno strumento musicale realizzato interamente in software.

Oscillatori, Esempi e frequenza

Il cuore dell'impianto di generazione suono di qualsiasi sintetizzatore di musica sono multipli oscillatori, così chiamati perché generano una forma d'onda più o meno periodica oscillante a una particolare frequenza e volume. Nella generazione di suoni per la musica, gli oscillatori che creare forme d'onda periodiche leali di solito suono piuttosto noiosi. Oscillatori più interessanti incorporano vibrato, tremolo o modifica timbri, e solo sono approssimativamente periodiche.

Un programma che si vuole creare oscillatori utilizzando XAudio2 comincia chiamando la funzione XAudio2Create. Questo fornisce un oggetto che implementa l'interfaccia IXAudio2. Da quell'oggetto è possibile chiamare CreateMasteringVoice solo una volta per ottenere un'istanza di IXAudio2MasteringVoice, che funziona come il mixer audio principale. Solo un IXAudio2MasteringVoice esiste in qualsiasi momento. Al contrario, generalmente denominerete CreateSourceVoice più volte per creare più istanze dell'interfaccia IXAudio2SourceVoice. Ognuna di queste istanze di IXAudio2SourceVoice può funzionare come un oscillatore indipendente. Combinare più oscillatori per uno strumento multiphonic, un'ensemble o un'intera orchestra.

Un oggetto IXAudio2SourceVoice genera suono creando e inviando buffer contenente una sequenza di numeri che descrivono una forma d'onda. Questi numeri sono spesso chiamati campioni. Spesso sono 16 bit (lo standard per CD audio) e sono venuto ad un tasso costante — solitamente 44.100 Hz (anche lo standard per CD audio) o giù di lì. Questa tecnica ha il nome di fantasia Pulse Code Modulation, o PCM.

Anche se questa sequenza di campioni in grado di descrivere una forma d'onda molto complessa, spesso un sintetizzatore genera un flusso abbastanza semplice di campioni — più comunemente un'onda quadra, onda un triangolo o un dente di sega — con una periodicità corrispondente alla frequenza della forma d'onda (percepita come passo) e un'ampiezza media che viene percepita come volume.

Ad esempio, se la frequenza di campionamento è 44.100 Hz, ed ogni ciclo di 100 campioni ha valori che ottenere progressivamente più grandi, quindi più piccolo, quindi negativo e torna a zero, la frequenza del suono risulta è 44.100 diviso per 100, o 441 Hz — una frequenza vicino al centro percettivo della gamma udibile per gli esseri umani. (Una frequenza di 440 Hz è la A sopra il do centrale e viene utilizzata come accordatura standard).

L'interfaccia IXAudio2SourceVoice eredita un metodo denominato SetVolume da IXAudio2Voice e definisce un metodo per la propria denominata SetFrequencyRatio. Sono stato particolarmente incuriosito da questo ultimo metodo, perché mi sembrava di fornire un modo per creare un oscillatore che genera una particolare forma d'onda periodica con una frequenza variabile con un minimo sforzo.

Figura 1 Mostra la maggior parte di una classe denominata SawtoothOscillator1 che implementa questa tecnica. Anche se io uso campioni familiare integer a 16 bit per definire la forma d'onda, XAudio2 utilizza internamente a 32-bit floating point campioni. Per le applicazioni critiche per le prestazioni, probabilmente vorrete esplorare le differenze di prestazioni tra interi e in virgola mobile.

Figura 1 molto della 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);
}

Nel file di intestazione, è impostare una frequenza di base che divide nettamente la frequenza di campionamento di 44.100. Da quello, una dimensione del buffer può essere calcolata che è la lunghezza di un singolo ciclo di una forma d'onda della frequenza:

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

Anche nell'intestazione del file è la definizione di tale buffer come un campo:

short waveformBuffer[BUFFER_LENGTH];

Dopo la creazione dell'oggetto IXAudio2SourceVoice, il dente di sega­Oscillator1 Costruttore riempie un buffer con un ciclo di una forma d'onda a dente di sega — una semplice forma d'onda che va da un'ampiezza di -32.768 a un'ampiezza di 32.767. Questo tampone è sottoposto a IXAudio2SourceVoice con le istruzioni che deve essere ripetuto per sempre.

Senza alcun ulteriore codice, questo è un oscillatore che riproduce un'onda a dente di sega 441 Hz per sempre. Che è grande, ma non è molto versatile. Per dare SawtoothOscillator1 un po ' più versatilità, ho incluso anche un metodo di SetFrequency. Questo argomento è una frequenza che la classe utilizza per chiamare SetFrequencyRatio. Il valore passato a SetFrequencyRatio può variare da valori float di XAUDIO2_MIN_FREQ_RATIO (o 1/1,024.0) fino a un valore massimo specificato precedentemente come argomento di CreateSourceVoice. Ho usato XAUDIO2_MAX_FREQ_RATIO (o 1,024.0) per quell'argomento. La gamma di udienza umana — circa 20 Hz e 20.000 Hz — è ben all'interno dei limiti definiti da quei due costanti applicati alla frequenza di base di 441.

Buffer e callback

Devo confessare che ero inizialmente un po ' scettico del metodo SetFrequencyRatio. Digitalmente aumentando e diminuendo la frequenza della forma d'onda non è un compito banale. Mi sono sentito obbligato a confrontare i risultati con una forma d'onda generata algoritmicamente. Questo è l'impeto dietro il progetto OscillatorCompare, che è tra i codici scaricabili per questa colonna.

Il progetto OscillatorCompare include la classe SawtoothOscillator1 che ho già descritto così come una classe SawtoothOscillator2. Questa seconda classe ha un metodo SetFrequency che controlla come la classe genera dinamicamente i campioni che definiscono la forma d'onda. Questa forma d'onda sono continuamente costruite in un buffer e presentate in tempo reale per l'oggetto IXAudio2SourceVoice in risposta a callback.

Una classe può ricevere i callback da IXAudio2SourceVoice implementando l'interfaccia IXAudio2VoiceCallback. Un'istanza della classe che implementa questa interfaccia viene quindi passata come argomento al metodo CreateSourceVoice. La classe SawtoothOscillator2 implementa questa interfaccia stessa e passa la propria istanza di CreateSourceVoice, indicando anche che non facendo uso di SetFrequencyRatio:

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

Una classe che implementa IXAudio2VoiceCallback può utilizzare il metodo OnBufferStart per essere notificato quando è il momento di presentare un nuovo buffer di dati di forma d'onda. Generalmente quando si utilizza OnBufferStart per mantenere aggiornati dati di forma d'onda, si vorrà mantenere un paio di tamponi e li si alternano. Questa è probabilmente la soluzione migliore se si stanno ottenendo dati audio da un'altra fonte, ad esempio un file audio. L'obiettivo è quello di non lasciare che il processore audio diventano "affamato". Mantenendo un buffer davanti al trattamento aiuta a prevenire la fame, ma non di garantirla.

Ma gravitato verso un altro metodo definito da IXAudio2VoiceCallback — OnVoiceProcessingPassStart. A meno che non stai lavorando con buffer molto piccole, in genere OnVoiceProcessingPassStart è chiamato più frequentemente OnBufferStart e indica quando un blocco di dati audio sta per essere elaborato e quanti byte sono necessari. Nella documentazione XAudio2, questo metodo di callback viene promosso come quella con la latenza più bassa, che spesso è altamente desiderabile per gli strumenti di musica elettronica interattiva. Non volete un ritardo tra la pressione di un tasto e di sentire la nota!

Il file di intestazione SawtoothOscillator2 definisce due costanti:

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

La prima costante è la lunghezza del buffer utilizzato per inviare i dati della forma d'onda. Qui funziona come un buffer circolare. Le chiamate al metodo OnVoiceProcessingPassStart richiedono un particolare numero di byte. Il metodo risponde mettendo quei byte nel buffer (a partire da dove lasciato l'ultima volta) e chiamando SubmitSourceBuffer solo per quel segmento aggiornato del buffer. Si desidera che questo buffer per essere sufficientemente grande, quindi il codice del programma non è sovrascrivendo la parte del buffer ancora riprodotto in sottofondo.

Si scopre che per una voce con una frequenza di campionamento di 44.100 Hz, chiamate al OnVoiceProcessingPassStart richiedono sempre 882 byte o 441 campioni a 16 bit. In altre parole, OnVoiceProcessingPassStart è chiamato ad un tasso costante di 100 volte al secondo, o ogni 10 ms. Anche se non documentata, questa durata 10 ms può essere trattata come un'elaborazione audio XAudio2 "quantum", ed è una figura buona da tenere a mente. Di conseguenza, il codice che scritto per questo metodo non può indugiare. Evitare chiamate API e chiamate di libreria runtime.

La seconda costante è la lunghezza di un singolo ciclo della forma d'onda desiderata. Potrebbe essere la dimensione di una matrice che contiene gli esempi di tale forma d'onda, ma in SawtoothOscillator2 è usato soltanto per i calcoli.

Il metodo SetFrequency in SawtoothOscillator2 utilizza quello costante per calcolare un incremento di angolo che è proporzionale alla frequenza desiderata della forma d'onda:

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

Anche se angleIncrement è un numero intero, viene trattata come se esso comprende parole integrali e frazionarie. Questo è il valore utilizzato per determinare ogni campione successivi dell'onda.

Si supponga, ad esempio, che l'argomento SetFrequency è 440 Hz. Il angleIncrement è calcolato come 5.356.535. In formato esadecimale, questo è 0x51BBF7, che viene trattato come un numero intero di 0x51 (o decimale 81), con una parte frazionaria di 0xBBF7, equivalente a 0.734. Se il ciclo completo di una forma d'onda è 8.192 byte e si utilizza solo la parte intera e saltare 81 byte per ogni campione, la frequenza risulta è circa 436.05 Hz. (Che è 44.100 volte 81 diviso da 8.192). Se si salta 82 byte, la frequenza risulta è di 441.43 Hz. Volete qualcosa tra queste due frequenze.

Questo è perché una parte frazionaria ha bisogno anche di inserire il calcolo. Il tutto sarebbe probabilmente più facile in virgola mobile, e in virgola mobile potrebbe anche essere più veloce su alcuni processori moderni, ma Figura 2 illustrato un approccio più "tradizionale" solo intero. Notare che solo la sezione aggiornata del buffer circolare è specificata con ogni chiamata a SubmitSourceBuffer.

Figura 2 OnVoiceProcessingPassStart in 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 e SawtoothOscillator2 possono essere confronto side-by-side nel programma OscillatorCompare. MainPage ha due coppie di dispositivi di scorrimento per modificare la frequenza e il volume di ciascun oscillatore. Il controllo Slider per la frequenza genera solo valori interi che vanno da 24 a 132. Ho preso in prestito questi valori dai codici utilizzati nel Musical strumento Digital Interface (MIDI) standard per rappresentare piazzole. Il valore di 24 corrisponde a C tre ottave sotto il do centrale, che è chiamato 1 C (C in ottava 1) in notazione scientifica passo e ha una frequenza di circa 32,7 Hz. Il valore di 132 corrisponde a una frequenza di circa 16.744 Hz C 10 e sei ottave sopra il do centrale. Un convertitore di tooltip su questi cursori viene visualizzato il valore corrente in notazione scientifica passo sia la frequenza equivalente.

Come ho sperimentato con questi due oscillatori, io non riuscivo a sentire una differenza. Ho anche installato un oscilloscopio software su un altro computer per esaminare visivamente le forme d'onda risultante, e non ho potuto vedere alcuna differenza neanche. Questo indica a me che la SetFrequency­rapporto metodo è implementato in modo intelligente, che naturalmente ci dovremmo aspettare in un sistema così sofisticati come DirectX. Ho il sospetto che stanno effettuandi interpolazioni su dati di forma d'onda ricampionata a spostare la frequenza. Se siete nervosi, è possibile impostare il BASE_FREQ molto basso — per esempio, a 20 Hz — e la classe genera una forma d'onda dettagliata costituito da 2.205 campioni. Può anche sperimentare con un alto valore: Per esempio, Hz 8.820 causerà una forma d'onda di soli cinque campioni per essere generato! Certo, questo ha un suono un po' diverso perché la forma d'onda interpolata si trova da qualche parte tra un dente e un'onda di triangolo, ma la forma d'onda risulta è ancora liscia senza "irregolarità".

Questo è di non implica che tutto funziona hunky dory. Con entrambi oscillatore a dente di sega, le ottave superiore coppia ottengono piuttosto caotiche. Il campionamento della forma d'onda tende ad emettere ad alta e bassa frequenza tratti di una sorta ho sentito prima, e che ho intenzione di indagare più a fondo in futuro.

Tenere il Volume!

Il metodo SetVolume definito da IXAudio2Voice ed ereditato da IXAudio2SourceVoice è documentato come un moltiplicatore a virgola mobile che può essere impostato su valori che vanno da -2 ^ 24-2 ^ 24, che è uguale a 16,777,216.

Nella vita reale, tuttavia, probabilmente vorrete tenere il volume su un oggetto IXAudio2SourceVoice per un valore compreso tra 0 e 1. Il valore corrisponde al silenzio di 0 e 1 corrisponde a nessun guadagno o attenuazione. Tenere presente che qualunque sia l'origine della forma d'onda associata con un IXAudio2SourceVoice — se viene generato algoritmicamente o ha origine in un file audio — ha probabilmente campioni a 16 bit che molto probabilmente si avvicinano i valori minimi e massimi di -32.768 e 32.767. Se si tenta di amplificare quelle forme d'onda con un livello di volume maggiore di 1, campioni superano la larghezza di un intero a 16 bit e verranno ritagliati i valori minimo e massimo. Distorsione e rumore comporterà.

Questo diventa critico quando si avvia la combinazione di più istanze di IXAudio2SourceVoice. Le forme d'onda di queste istanze multiple sono mescolate da essere aggiunti insieme. Se si consente a ognuna di queste istanze per avere un volume di 1, la somma delle voci molto bene potrebbe causare campioni che superano le dimensioni dei numeri interi a 16 bit. Questo potrebbe accadere sporadicamente — solo con conseguente distorsione intermittente — o cronicamente, risultante in un vero e proprio pasticcio.

Quando si utilizzano più istanze di IXAudio2SourceVoice che generano forme d'onda di piena 16-bit di ampiezza, una misura di sicurezza è impostato il volume di ciascun oscillatore a 1 diviso per il numero di voci. Che garantisce che la somma non supera mai un valore di 16 bit. Una regolazione del volume complessivo può essere fatto anche tramite la voce di mastering. Si potrebbe anche voler esaminare la funzione XAudio2CreateVolumeMeter, che consente di creare un oggetto di elaborazione audio che può aiutare a volume monitor per scopi di debugging.

Il nostro primo strumento musicale

È comune per gli strumenti musicali su tavolette di avere una tastiera di pianoforte-stile, ma sono stato incuriosito recentemente da un tipo di tastiera tasto trovato sulle fisarmoniche come il bayan russo (che ho familiarità con il lavoro della compositrice russa Sofia Gubaidulina). Perché ogni tasto è un tasto piuttosto che una lunga leva, molti più tasti possono essere imballati all'interno dello spazio limitato dello schermo del tablet, come mostrato Figura 3.

The ChromaticButtonKeyboard Program
Figura 3 il programma ChromaticButtonKeyboard

Il fondo due righe duplicano le chiavi su due righe superiore e sono fornite per facilitare la diteggiatura di sequenze melodiche e accordi comuni. In caso contrario, ogni gruppo di 12 tasti nelle tre righe superiore forniscono tutte le note dell'ottava, generalmente crescente da sinistra a destra. La gamma totale qui quattro ottave, che è circa due volte quello che si otterrebbe con una tastiera di pianoforte della stessa dimensione.

Un vero bayan ha un'ottava supplementare, ma non riuscivo a montare senza fare i tasti troppo piccoli. Il codice sorgente consente di impostare le costanti per provare quella ottava supplementare, o per eliminare un'altra ottava e anche ingrandire i pulsanti.

Perché non posso pretendere che questo programma suona come qualsiasi strumento che esiste nel mondo reale, ho semplicemente chiamato ChromaticButton­tastiera. I tasti sono istanze di un controllo personalizzato denominato chiave che deriva da ContentControl ma esegue qualche tocco di elaborazione per mantenere una proprietà IsPressed e generare un evento IsPressedChanged. La differenza tra il tocco gestisce in questo controllo e il tocco di manipolazione in un normale pulsante (che ha anche una proprietà IsPressed) è evidente quando si spazza il dito attraverso la tastiera: Un pulsante standard verrà impostata la proprietà IsPressed su true solo se la stampa dito si verifica sulla superficie del pulsante, mentre questo controllo chiave personalizzato considera il tasto da premere se un dito spazza dal lato.

Il programma crea sei istanze di una classe SawtoothOscillator che è praticamente identica alla classe SawtoothOscillator1 dal progetto precedente. Se il tuo touchscreen lo supporta, puoi giocare sei note simultanee. Non ci sono nessun richiamate e la frequenza dell'oscillatore è controllata dalle chiamate al metodo SetFrequencyRatio.

Per tenere traccia di quali oscillatori sono disponibili e quali oscillatori stanno giocando, il file MainPage.xaml.h definisce due oggetti insieme standard come campi:

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

Ogni oggetto chiave aveva originariamente, la relativa proprietà Tag impostata nel codice di nota MIDI che discusso in precedenza. Che è come il gestore di IsPressedChanged determina quale tasto viene premuto e quale frequenza di calcolare. Tale codice MIDI è stato utilizzato anche come chiave per la collezione playingOscillators mappa. Ha funzionato bene fino a quando ho suonato una nota da due righe di fondo che duplicato una nota già giocando, che ha portato una chiave duplicata e un'eccezione. Ho risolto facilmente questo problema inserendo un valore nella proprietà etichetta che indica la riga in cui si trova la chiave: L'etichetta è uguale ora il codice di nota MIDI oltre 1.000 volte il numero di riga.

Figura 4 viene illustrato il gestore di IsPressedChanged per le istanze di chiave. Quando viene premuto un tasto, un oscillatore è rimosso dalla collezione availableOscillators, dato una frequenza e volume diverso da zero e mettere nella raccolta playingOscillators. Quando viene rilasciato un tasto, l'oscillatore è dato un volume zero e tornò a availableOscillators.

Figura 4 il gestore di IsPressedChanged per le istanze di chiave

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);
    }
  }
}

Che è circa così semplice come può essere uno strumento a più voci, e naturalmente esso è imperfetto: Suoni dovrebbero non essere attivati e disattivata come un interruttore. Il volume dovrebbe scivolare rapidamente ma senza problemi quando inizia una nota e la caduta indietro quando si ferma. Molti strumenti reali hanno anche un cambiamento di volume e timbro con il progredire della nota. C'è ancora spazio per miglioramenti.

Ma considerando la semplicità del codice, esso funziona sorprendentemente bene ed è molto reattivo. Se si compila il programma per il processore ARM, è possibile distribuirlo sul braccio base Microsoft Surface e passeggiata intorno cullando la tavoletta untethered in un braccio giocando su di esso con l'altra mano, che devo dire è un po ' di un brivido.

Charles Petzold è un collaboratore di lunga data di MSDN Magazine e autore di "Programming Windows, 6a edizione" (o ' Reilly Media, 2012), un libro sulla scrittura di applicazioni per Windows 8. Il suo sito Web è charlespetzold.com.

Grazie ai seguenti esperti tecnici per la revisione di questo articolo: Tom Mathews e Thomas Petchel