Neue Benutzeroberflächentechnologien

Tonaufnahmen mit Windows Phone 7

Charles Petzold

Beispielcode herunterladen.

image: Charles Petzold In einer der ersten gedruckten Anzeigen zur Einführung des Macintosh 1984 bewarb Apple das Design seiner Maus mit einer ganz besonders überzeugenden Bemerkung: "Manche Mäuse haben zwei Tasten. Die Macintosh-Maus hat nur eine. Es ist also wirklich schwer, auf die falsche Taste zu drücken."

Dies ist natürlich nicht wirklich die ganze Wahrheit. Wenn eine einzige Taste mit mehreren Funktionen überfrachtet wird, kann dies natürlich genau so verwirrend sein wie mehrere tasten. Die Unmöglichkeit, auf die falsche Taste zu drücken, ist jedoch sicherlich ein überzeugendes Argument für die Einfachheit eines Benutzeroberflächendesigns.

Die Benutzeroberfläche auf das Wesentliche zu beschränken, ist besonders wichtig bei der Programmierung für Smartphones. Telefone sind nicht sehr groß. Sie können einfach nicht sehr viele Tasten haben, und die Finger, die man dafür verwendet, agieren nicht so präzise wie ein Mauszeiger. Zu viele Tasten führen dazu, dass es sogar besonders "einfach" ist, die falsche zu drücken.

Auf der anderen Seite führt eine abgespeckte Benutzeroberfläche oft dazu, dass das Programm weniger Funktionen bietet – es ist hier oft nicht einfach, zu einem ausgewogenen Verhältnis zu finden. Das Leben ist voller Kompromisse.

Design-Evolution

Ich fand es reizvoll, ein Programm für Windows Phone 7 zu schreiben, das die Aufzeichnung kurzer Sprachnotizen ermöglicht, wie etwa "An die Wäsche denken", oder "Großartige Idee für einen Film: Mann trifft Frau."

Ein solches Programm ist natürlich nützlich, und es bietet eine weitere Möglichkeit, in der Öffentlichkeit mit unseren Windows Phones zu prahlen. Wichtiger für mich war jedoch, dass dies eine sehr gute Gelegenheit wäre, praktische Erfahrungen mit den Audioaufzeichnungs- und Abspielfunktionen des Geräts zu sammeln.

Das Programmdesign erwies sich jedoch als problematischer als ich anfangs gedacht hatte. bevor ich auch nur eine einzige Codezeile geschrieben hatte, durchlief das Programm mehrere Design- und Redesign-Phasen in meinem Kopf.

Zuerst dachte ich, es wäre am besten, nur zwei Schaltflächen, "Aufzeichnen" und "Abspielen" zu haben, die beide als Umschalter fungieren würden. Druck auf die Schaltfläche "Aufzeichnen" startet die Aufzeichnung, erneuter Druck hält die Aufzeichnung an. Das Programm sichert die Audiodaten in einem isolierten Speicher. Druck auf die Schaltfläche "Abspielen" spielt die Aufzeichnung ab. Bei jedem Druck auf die Schaltfläche "Aufzeichnen" wird die vorherige Aufzeichnung ersetzt, so dass keine Schaltfläche "Löschen" benötigt wird.

Ich spielte sogar mit dem Gedanken, das Programm nur auf eine Schaltfläche "Abspielen" zu reduzieren, indem ich eine Sprachaktivierungsfunktion einführte! Das Programm würde dann kontinuierlich aufzeichnen und die Daten nur dann speichern, wenn Audioinhalte vorhanden sind. Es erwies sich jedoch als mörderisch schwierige Aufgabe, Hintergrundgeräusche ohne irgendeine Art von manueller Schwelleneinstellung von echten Sprachdaten zu unterscheiden. So gab ich den Gedanken an eine einzige Schaltfläche wieder auf.

Mein ursprünglicher Plan war für eine einzige Sprachnotiz OK, nicht jedoch für mehrere. Dann dachte ich darüber nach, dass das Programm eine einzige Audiodatei speichern und alle neuen Sprachnotizen immer hinten anhängen würde. Da dann alles eine einzige große Datei wäre, würde die Schaltfläche "Abspielen" alle Notizen hintereinander abspielen. Natürlich kann das Programm diese Datei nicht unendlich groß werden lassen, so dass hierbei unbedingt eine Schaltfläche zum Löschen der gesamten Datei, und damit aller Sprachnotizen, benötigt würde.

Das war auch nicht wirklich gut. Ich musste schließlich doch für jede Sprachnotiz eine einzelne Datei vorsehen, die sämtlich separat gelöscht werden konnten. Dann brauchte ich aber auch eine Möglichkeit, alle diese separaten Dateien dem Benutzer zum Abspielen oder Löschen zu präsentieren, und schon wurde mein kleines Programm richtig komplex. Ich brauchte unbedingt ein ListBox-Element und eine Möglichkeit für den Benutzer, die einzelnen Notizen zu identifizieren, vielleicht mit benutzerdefinierten Stichwörtern oder – der absolute Schrecken – einem echten Dateinamen.

Nein, nein, nur das nicht! Ich schaute hinüber zu meinem Anrufbeantworter. jeder Anruf und jede Sprachnotiz wird separat aufgezeichnet, diese werden jedoch auf dem einfachen Display nummeriert. Die "Abspielen"-Schaltfläche wird durch "Weiter"- und "Zurück"-Schaltflächen ergänzt, die die Weiterschaltung zum nächsten oder vorherigen Anruf ermöglichen. Wenn eine Notiz oder ein Anruf gelöscht wird, findet jedoch eine Neunummerierung statt. Ich wusste, dass ich keine Nummerierung der Sprachaufzeichnungen wollte, ich konnte jedoch das größere Display auf dem Telefon nutzen, um mehr Details zu jeder Notiz anzuzeigen, einschließlich des Aufzeichnungsdatums, der Dauer und der Dateigröße.

Der wirkliche Durchbruch kam , als ich erkannte, dass ich die ListBox des Programms auf den Hauptbildschirm setzen und nicht nur für die Auswahl, sondern auch für die Abspielfunktion verwenden konnte.

Verwenden des Programms

Mein endgültiges Design war dann, natürlich, ein Kompromiss zwischen äußerster Einfachheit und einem vollständigen notizengesteuerten System. Das zum Download verfügbare SpeakMemo-Projekt wurde für Silverlight für Windows Phone geschrieben und erfordert die Windows Phone 7 Development Tools. Sie können das Programm auf dem Telefonemulator ausführen, und es sieht dort hervorragend aus, kann aber keine Audiodaten aufzeichnen oder abspielen.

Bei der ersten Ausführung zeigt das SpeakMemo-Programm den Bildschirm aus Abbildung 1.

image: The Initial SpeakMemo

Abbildung 1 Der anfängliche SpeakMemo-Bildschirm

Eine Schaltfläche! Oder wenigstens nur eine aktivierte Schaltfläche auf einem nicht sehr überladenen Bildschirm. Die Schaltfläche zeigt, wie viel isolierter Speicherplatz vorhanden ist, und in welchem Verhältnis dies zu der aufgezeichneten Audiodatei steht. (Nein, das Programm ermöglicht nicht die Aufzeichnung von 10 Stunden am Stück!)

Drücken Sie auf die Schaltfläche "Aufzeichnen", und diese wird zu einem rot blinkenden Display mit einer sich aktualisierenden Daueranzeige, wie in Abbildung 2.

image: SpeakMemo While Recording

Abbildung 2 SpeakMemo bei der Aufzeichnung

Drücken Sie erneut auf die Schaltfläche "Aufzeichnen", und die aufgezeichnete Notiz wird mit Datum und Uhrzeit, Dauer, Speicherplatz und der Schaltfläche "Abspielen" angezeigt, wie in Abbildung 3.

image: SpeakMemo with One Memo

Abbildung 3 SpeakMemo mit einer Sprachnotiz

Natürlich können Sie jederzeit die Schaltfläche "Abspielen" verwenden, sie schaltet zwischen den Modi "Abspielen" und "Pause" um.

Bei nur einer Sprachnotiz ist dies nicht so offensichtlich, aber die aufgezeichneten Sprachnotizen werden in einer ListBox in umgekehrter chronologischer Reihenfolge gespeichert, wie in Abbildung 4 gezeigt; wenn Sie viele Sprachaufzeichnungen haben, können Sie sie durchlaufen und einzeln abspielen.

image: The SpeakMemo ListBox

Abbildung 4 Die SpeakMemo-ListBox

Eine der leistungsstarken Funktionen von Silverlight ist das DataTemplate, mit dessen Hilfe Sie das Erscheinungsbild von Elementen in einer ListBox definieren können. Dieses DataTemplate kann weitere Steuerelemente enthalten, wie etwa Schaltflächen. Ich war stolz darauf, eine praktische Anwendung für die Einfügung einer Schaltfläche in einem DataTemplate gefunden zu haben.

Sie können die gesammelten Sprachnotizen auch verwalten, indem Sie einzelne davon löschen. Bei Auswahl einer Sprachnotiz wird die Schaltfläche "Löschen" aktiviert. Möglicherweise durch das Setzen einer Schaltfläche in einem DataTemplate inspiriert führte ich einen weiteren Silverlight-Trick durch und fügte zwei zusätzliche Schaltflächen in die Schaltfläche "Löschen" ein. Diese Schaltflächen werden sichtbar, wenn Sie auf "Löschen" drücken, und sie führen die herkömmliche Bestätigungsfunktion aus, wie in Abbildung 5 gezeigt.

image: Confirming a Delete

Abbildung 5: Bestätigung eines Löschvorgangs

Wenn eine Sprachnotiz abgespielt wird, wird sie ausgewählt, ein Element wird jedoch nicht abgespielt, wenn Sie es durch Drücken auf den Bereich rechts von der Abspielschaltfläche auswählen. Das Programm lässt Sie eine Sprachnotiz abspielen, eine weitere aufzeichnen und eine dritte löschen – alles gleichzeitig.

Das Telefon und Audio

Ursprünglich war geplant, Windows Phone 7 mit einigen der Spracherkennungs- und -synthesefunktionen auszustatten, die in den Microsoft .NET Framework System.Speech-Namespaces geboten werden. Vielleicht kommt dies ja eines Tages.

Bis dahin können Sie Audioinhalte über das Mikrofon des Telefons erfassen und über den Lautsprecher abspielen; dazu werden die Klassen in den Microsoft.Xna.Framework.Audio-Namespaces verwendet. Dabei handelt es sich um XNA-Klassen, sie können jedoch auch in Silverlight-Programmen verwendet werden. Um XNA-Klassen in einem Silverlight-Projekt zu verwenden, fügen Sie einfach Microsoft.Xna.Framework.dll eine Referenz zu den Referenzen des Projekts hinzu, und ignorieren Sie die Warnmeldung.

Die Klassen im Microsoft.Xna.Framework.Audio-Namespace sind vollständig von denen im Microsoft.Xna.Framework.Media-Namespace getrennt. Der Media-Namespace enthält Klassen für das Abspielen von Musik aus der Musikbibliothek des Telefons, wobei es sich um Audiodateien im MP3- oder WMA-Format handelt, die zu Objekten des Typs "Song" werden. In Kapitel 18 meines Buches “Programming Windows Phone 7” (Microsoft Press, 2010), das Sie auf bit.ly/dr0Hdz kostenlos herunterladen können, zeige ich, wie Sie auf die Musikbibliothek zugreifen können. In einem Blogeintrag auf meiner Website zeige ich auch, wie man MP3- oder WMA-Dateien abspielt, die in dem Programm selbst gespeichert sind, oder die aus dem Internet heruntergeladen werden können (bit.ly/ea73Fz).

Demgegenüber arbeiten Klassen im Microsoft.Xna.Framework.Audio-Namespace mit nichtkomprimierten Audiodaten im Standard-PCM-Format – diese Methode wird auch für Audio-CDs und Windows WAV-Dateien verwendet. Bei PCM wird die analoge Audioamplitude bei einer einheitlichen Rate gesampelt (normalerweise zwischen 8.000 und 48.000 Samples pro Sekunde), und jedes Sample wird gewöhnlich als 8- oder m16-Bit-Wert gespeichert. Der Speicherbedarf für einen bestimmten Audiosound ist dann das Produkt aus der Dauer in Sekunden, der Samplingrate und der Bytezahl pro Sample (mal zwei für Stereosound).

Wenn Sie in Ihrer Windows Phone 7-Anwendung Spracherkennung benötigen, müssen Sie sie selbst hinzufügen, am besten über einen Webservice. Auch ein Programm, das text in Sprache umwandelt, nutzt wahrscheinlich einen Webservice – oder man muss warten, bis die Telefone diese Unterstützung bieten. Die Microsoft Translator-App für Windows Phone tut dies durch Verwendung des Microsoft Translator-Dienstes (microsofttranslator.com). Den Code und die Dokumentation für das Translator Starter Kit finden Sie auf MSDN (msdn.microsoft.com/library/gg521144(VS.92).aspx) und AppHub (create.msdn.com/education/catalog/sample/translatorstarterkit).

Bei der Verwendung von XNA-Audiodiensten muss ein Silverlight-Programm die statische FrameworkDispatcher.Update-Methode mit etwa der Rate aufrufen, die der Video-Aktualisierungsrate entspricht, d.°h. bei Windows Phone 7 etwa 30 mal pro Sekunde. Der Artikel “Enable XNA Framework Events in Windows Phone Applications (Aktivieren von XNA Framework Events in Windows Phone-Anwendungen)" in der XNA-Onlinedokumentation (msdn.microsoft.com/library/ff842408) enthält eine Anleitung dazu. In SpeakMemo übernimmt die the XnaFrameworkDispatcherService-Klasse diese Aufgabe. Sie wird in der Datei App.xaml instantiiert.

Audio-Aufzeichnung

Um über das Mikrofon des Telefons Audioinhalte aufzuzeichnen, verwenden Sie die Klasse Microphone. Möglicherweise erstellen Sie eine Instanz dieser Klasse mit der statischen Default-Eigenschaft:

Microphone microphone = Microphone.Default;

Als Alternative bietet die statische All-Eigenschaft eine Sammlung von Microphone-Objekten, Sie möchten die Liste aber wahrscheinlich dem Benutzer zur Auswahl präsentieren.

Die Samplingrate ist fest, kann nicht geändert werden und wird von der Eigenschaft SampleRate als 16.000 Samples pro Sekunde angegeben. Nach dem Nyquist-Abtasttheorem ist dies für die Aufzeichnung von Audioinhalten bis zu einer Frequenz von 8.000 Hz geeignet. Für Sprache ist dies ausreichend, man sollte damit jedoch nicht hervorragende Ergebnisse bei Musik erwarten. Jedes Sample hat eine Breite von 2 Byte und ist monaural, das heißt, für jede aufgezeichnete Sekunde werden 32.000 Byte benötigt (und für jede Minute 1,9 MB).

Die Microphone-Daten werden Ihrem Programm in Pufferblöcken übergeben, bei denen es sich einfach um Byte-Arrays handelt. Sie installieren einen Handler für das Bufferreader-Ereignis und rufen dann "Start auf, um die Aufzeichnung zu starten. Wenn das Microphone-Objekt das BufferReady-Ereignis ausgibt, ruft Ihr Code getData mit einem Byte-Array auf. Bei der Rückgabe von GetData wurde der Puffer mit PCM-Daten gefüllt. Wenn das Programm die Aufzeichnung anhalten will, rufen Sie erneut GetData auf, um den letzten Teilpuffer abzurufen. Die Methode gibt die Zahl der transferierten Bytes an das Array zurück. Dann wird Stop aufgerufen.

Die einzige Option, die Microphone Ihnen gibt, besteht in der Angabe der Größe (in Byte) des Puffers, der an GetData übergeben wird. Die Eigenschaft BufferSize ist ein TimeSpan-Wert, der zwischen 100 und 1.000 ms (eine Sekunde), mit Schritten von 10 ms, liegen muss. In SpeakMemo habe ich den Standardwert von 1.000 unverändert gelassen.

Um Ihnen die Arbeit zu erleichtern, bietet die Microphone-Klasse zwei Möglichkeiten für die Umwandlung von Puffergrößen und Zeit. Leider sind diese Methoden etwas verwirrend, da sich die Namen auf "Sample" beziehen. Die Methode GetSampleDuration dividiert eine Bytegröße durch 32.000 und gibt dann einen TimeSpan-Wert aus, die das Ergebnis als Sekunden angibt. GetSampleSizeInBytes multipliziert einen TimeSpan-Wert (in Sekunden) mit 32.000.

Wenn SpeakMemo Audioinhalte aufzeichnet, akkumuliert es mehrere 32.000-Byte-Puffer in einer generischen List-Sammlung. Beim Anhalten der Aufzeichnung werden alle einzelnen Puffer in einer Datei in einem isolierten Speicher gespeichert.

Sobald ich entschieden hatte, dass ich für die Identifizierung der Sprachnotizen keine Stichwortfunktion verwenden würde, wollte ich, dass die Datei nur die PCM-Daten, ohne weitere Informationen, enthielt. ich war aber sehr überrascht, als ich erkannte, dass die Klasse IsolatedStorageFile in Silverlight für Windows Phone die Methoden für den Zugriff auf die Dateierstellungszeit oder den Zeitpunkt der letzten Schreibänderung nicht unterstützte, da ich diese Funktion für absolut wichtig für den Benutzer halte.

Dies bedeutete, dass der Dateiname selbst Datum und Uhrzeit enthalten musste. Ich versuchte zuerst, einen Dateinamen aus dem DateTime-Objekt zu erstellen (mit den "s"- und "u"-Formatierungsoptionen), dies führte allerdings zu nichts. (Warum dies nicht funktioniert, können Sie im Rahmen einer einfachen Übung selbst herausfinden). Ich erstellte mir dann einen eigenen Dateinamenstring, indem ich die einzelnen Komponenten für Datum und Uhrzeit zusammenfügte.

XNA-Audiowiedergabe

Mit dem Microsoft.Xna.Framework.Audio-Namespace können Sie aufgezeichnete Audioinhalte mit den verwandten Klassen SoundEffect und SoundEffectInstance abspielen, deren Namen im Kontext eines XNA-Soiels sicherlich für sich selbst sprechen! Die Statische Methode SoundEffect.FromStream erfordert jedoch ein Stream-Objekt, das eine Standard- Windows WAV-Datei, vollständig mit RIFF-Header, referenziert, und ich wollte mich nicht mit Dateiformaten herumschlagen.

Für die Arbeit mit rohen PCM-Daten anstelle von WAV-Dateien verwendet man stattdessen die Klasse DynamicSoundEffectInstance, die von SoundEffectInstance abgeleitet ist. Diese Klasse ist ideal für von der Microphone-Klasse generierte Daten geeignet, oder auch für Programme, die ihre eigenen Waveform-Daten dynamisch generieren, wie etwa Musiksynthesizerprogramme.

Der DynamicSoundEffectInstance-Konstruktor erfordert eine Samplingrate und die Zahl der Kanäle; wenn Sie sie mit vom Mikrofon generierten Daten verwenden, müssen Sie diese natürlich konsistent halten:

DynamicSoundEffectInstance playback = 
  new DynamicSoundEffectInstance(
  microphone.SampleRate, AudioChannels.Mono);

Anderseits: Wenn Sie möchten, dass der abgespielte Sound wie ein schnell sprechendes Eichhörnchen klingt, multiplizieren Sie einfach dieses erste Argument mit dem Faktor zwei. DynamicSoundEffectInstance erwartet eine Samplegröße von 16 Bit. Die Klasse verfügt über Abspielen-, Pause-, Fortsetzen- und Halt-Methoden zur Steuerung des Abspielens, sowie über eine Eigenschaft State zur Anzeige des aktuellen Status. In gewisser Weise funktioniert die Klasse in gegensätzlicher Weise wie die Klasse Microphone: Sie gibt ein BufferNeeded-Ereignis aus, wenn neuer Puffer benötigt wird. Sie müssen dann den Puffer mit PCM-Daten füllen und SubmitBuffer aufrufen.

Um hörbare Lücken in der Sprachaufnahme zu vermeiden, sollten Sie eine Puffer-Queue in der DynamicSoundEffectInstance-Klasse haben und einen neuen Puffer übergeben, während der vorherige Puffer noch abgespielt wird. Die Klasse bietet dazu eine Eigenschaft PendingBufferCount, die die Zahl der in der Queue stehenden Puffer angibt. Das Bufferneeded-Ereignis wird ausgegeben, wenn die PendingBufferCount-Zählung sich ändert und kleiner oder gleich zwei ist.

Wenn Sie jedoch nur einen einzigen Block PCM-Daten abspielen müssen, kann SubmitBuffer auch aufgerufen werden, ohne dass das BufferNeeded-Ereignis dabei eine Rolle spielen muss. Zuerst habe ich im SpeakMemo-Programm die Klasse auf diese Weise verwendet, dann wurde mir jedoch klar, dass es nicht möglich war, festzustellen, wann das Abspielen des Puffers beendet war. Es gibt kein "Status geändert"-Ereignis, und auch, wenn dies der Fall wäre, würde DynamicSoundEffectInstance nicht von Zustand "Abspielen" zum Zustand "Halt" umschalten, wenn das Abspielen des Puffers beendet ist. Es erwartet immer weitere Puffer. Wenn das Programm diese Informationen nicht hat, kann es die visuelle Anzeige der "Abspielen/Pause"-Schaltfläche nicht umschalten.

Schließlich nutzte ich das BufferNeeded-Ereignis, aber nur, um damit die Eigenschaft PendingBufferCount zu prüfen. Wenn PendingBufferCount Null erreicht, ist das Abspielen des Puffers beendet.

Speicherprobleme

SpeakMemo speichert aufgezeichnete Sprachnotizen in einem isolierten Speicher. Konzeptionell isolierter Speicher steht nur der Anwendung zur Verfügung, physisch ist er jedoch Teil des gesamten Speicherbereichs, der der Festplatte eines Desktop-Computers entspricht. Alle ausführbaren Dateien der Anwendung werden hier gespeichert, wie auch die Fotobibliothek, die Videobibliothek und vieles mehr. Die Hardwarespezifikation für Windows Phone 7 erfordert, dass das Telefon über mindestens 8 GB Flash-Speicher für diesen Speicherbereich verfügt, und dass das Telefon selbst den Benutzer informiert, wenn dieser Speicherplatz zur Neige geht.

Die Speicherung der Sprachnotizdateien war nicht mein Hauptproblem. Mir ging es mehr über den Heapspeicher des Programms. Abgesehen vom Flash-Speicher erfordert die Hardwarespezifikation von Windows Phone 7 aus 256 MB RAM. Dies ist der Speicher, den eine Anwendung bei ihrer Ausführung beansprucht, und der den lokalen Heapspeicher des Programms zur Verfügung stellt. Meine Experimente zeigten, dass SpeakMemo ein Array bis zu 90 MB verarbeiten kann, bevor eine Out-of-memory-Ausnahme ausgegeben wird. Dies entspricht Audioaufzeichnungen über das Mikrofon in einer Länge von 47 Minuten.

Dies bedeutet nicht, dass ein Windows Phone 7-Programm notwendigerweise auf eine Aufzeichnungsdauer von 47 Minuten beschränkt ist. Ein Programm, dass so lange Aufzeichnungen bewältigen soll, muss jedoch ständig Puffer in isolierten Speicherbereichen ablegen, um Speicherplatz freizumachen, und beim Abspielen die Datei dann in inkrementeller Weise laden. So war SpeakMemo nicht strukturiert. Stattdessen speicherte und lud das Programm ganze Dateien, und ich wollte diese viel einfachere Struktur nicht aufgeben.

Aus diesem Grunde habe ich für die Dauer der Sprachnotizen eine Obergrenze von 10 Minuten eingeführt. Sobald eine Aufzeichnung diese Länge erreicht, wird sie ganz einfach angehalten (was selbst einige Sekunden dauert). Um das Programm einfach zu halten, wird keine Warnmeldung ausgegeben. Die Aufzeichnung wird einfach angehalten, als ob der Benutzer auf die entsprechende Schaltfläche gedrückt hätte. Diese automatische Stop-and-Save-Funktion wird auch aufgerufen, wenn das Programm beendet oder auf andere Weise deaktiviert wird, etwa beim Tombstoning.

Natürlich ist das Abspielen einer zehnminütigen Sprachaufzeichnung nicht wirklich komfortabel. Die Schaltfläche "Abspielen" schaltet zwischen den Modi "Abspielen" und "Pause" um, es gibt jedoch keine Möglichkeit zum Zurück- oder Vorspulen. Solche Funktionen könnten durchaus hinzugefügt werden, Sie wissen aber schon, was dafür erforderlich ist?

Ja: Mehr Schaltflächen. Oder sogar ein Schieberegler.

Charles Petzoldschreibt seit langem redaktionelle Beiträge für das MSDN Magazin*. Sein neues Buch, „Programming in Windows Phone 7“ (Programmierung in Windows Phone 7, Microsoft Press 2010) ist als kostenloser Download auf bit.ly/dr0Hdz.verfügbar.*

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Mark Hopkins