Touch and Go

Audiostreaming für Windows Phone

Charles Petzold

Charles PetzoldOb sie nun auf dem Desktop, im Internet oder mobil ausgeführt werden: Manchmal sollen Computerprogramme Sounds oder Musik abspielen. Meistens sind diese Audiodaten in MP3- oder WMA-Dateien codiert. Der große Vorteil dieser Methode liegt darin, dass das Betriebssystem in der Regel weiß, wie diese Dateien decodiert und abgespielt werden müssen. Die Anwendung kann sich somit auf den vergleichsweise leichten Job der UI-Bereitstellung für Pause, erneute Wiedergabe und die Navigation zwischen den Tracks konzentrieren.

Aber es ist nicht immer so einfach. Manchmal muss das Programm eine Audiodatei abspielen, deren Format nicht vom Betriebssystem unterstützt wird, oder sogar Audiodaten dynamisch generieren, beispielsweise, um elektronisch synthetisierte Musik zu implementieren.

Im Sprachgebrauch von Silverlight und Windows Phone wird dieser Prozess als Audiostreaming bezeichnet. Zur Laufzeit wird von der Anwendung ein Bytestream bereitgestellt, der die Audiodaten enthält. Dazu wird eine aus MediaStreamSource abgeleitete Klasse genutzt, mit der die Audiodaten auf Abruf an den Audioplayer des Betriebssystems übermittelt werden. Mit Windows Phone OS 7.5 kann das Audiostreaming im Hintergrund erfolgen. Ich zeige Ihnen, wie das funktioniert.

Ableiten aus der MediaStreamSource-Klasse

Der erste wichtige Schritt bei der dynamischen Generierung von Audiodaten in einem Windows Phone-Programm besteht im Ableiten aus der abstrakten MediaStreamSource-Klasse. Der dazu erforderliche Code ist teilweise recht chaotisch, sodass Sie ihn wohl eher von jemandem übernehmen, als den Code selbst zu schreiben.

Das SimpleAudioStreaming-Projekt im herunterladbaren Quellcode für diesen Artikel stellt einen möglichen Ansatz dar. Dieses Projekt enthält eine MediaStreamSource-Ableitung mit der Bezeichnung Sine440AudioStreamSource, die ganz einfach ein Sinussignal bei 440 Hertz erzeugt. Diese Frequenz entspricht dem Kammerton A über dem mittleren C und wird als Standardstimmton verwendet.

MediaStreamSource verfügt über sechs abstrakte Methoden, die eine Ableitung überschreiben muss, aber nur zwei davon sind entscheidend. Die eine der beiden ist OpenMediaPlayer, in dieser Methode sind einige Dictionary-Objekte sowie ein List-Objekt zu erstellen, außerdem müssen der Typ der von der Klasse bereitgestellten Audiodaten und mehrere Parameter für die Datenbeschreibung definiert werden. Die Audioparameter werden als Felder einer Win32-WAVEFORMATEX-Struktur abgebildet, bei der alle Multibyte-Zahlenwerte im Little-Endian-Format (unwichtigster Byte zuerst) in eine Zeichenfolge konvertiert werden.

Ich habe MediaStreamSource nie für etwas anderes als Audio im PCM-Format (Puls-Code-Modulation) verwendet, das für die meisten unkomprimierten Audiodaten eingesetzt wird, darunter auch CDs und das WAV-Dateiformat von Windows. Bei PCM-Audiodaten sind gleich große Samples mit einer konstanten Rate erforderlich, die als Samplerate bezeichnet wird. Für einen Sound in CD-Qualität werden 16 Bit pro Sample und eine Samplerate von 44.100 Hz verwendet. Sie können entweder einen Kanal für den Monoklang oder zwei Kanäle für den Stereoklang wählen.

Bei der Sine440AudioStreamSource-Klasse sind ein Kanal und eine Samplegröße von 16 Bit hartcodiert, die Samplerate kann jedoch als Konstruktorargument angegeben werden.

Intern hält die Audio-Pipeline einen Puffer an Audiodaten, dessen Größe von der AudioBufferLength-Eigenschaft der MediaStreamSource-Klasse festgelegt wird. Der Standardwert beträgt 1000 ms, er kann aber bis auf 15 ms heruntergesetzt werden. Um den Puffer voll zu halten, wird die GetSampleAsync-Methode der MediaStreamSource-Ableitung aufgerufen. Ihre Aufgabe besteht darin, eine Ladung Audiodaten in einen MemoryStream zu schaufeln und ReportGetSampleCompleted aufzurufen.

Die Hartcodierung der Sine440AudioStreamSource-Klasse ist so ausgerichtet, dass sie 4096 Samples pro Aufruf bereitstellt. Bei einer Samplerate von 44.100 ist das nicht ganz eine Zehntelsekunde Audio pro Aufruf. Wenn Sie einen benutzergesteuerten Synthesizer implementieren möchten, der schnell auf Benutzereingaben reagieren soll, muss mit diesem Wert und der AudioBufferLength-Eigenschaft getüftelt werden. Um eine minimale Latenz zu erreichen, sollte der Puffer klein gehalten werden – aber nicht so klein, dass bei der Wiedergabe Lücken entstehen.

In Abbildung 1 wird die Sine440AudioStreamSource-Implementierung der GetSampleAsync-Überschreibung dargestellt. In der Schleife wird durch einen Aufruf der Math.Sin-Methode ein 16-Bit-Sinuswert erhalten, der auf die folgende short-Größe skaliert wird:

short amplitude = (short)(short.MaxValue * Math.Sin(angle));

Abbildung 1 Die GetSampleAsync-Methode in „Sine440AudioStreamSource“

protected override void GetSampleAsync(MediaStreamType mediaStreamType)
{
  // Reset MemoryStream object
  memoryStream.Seek(0, SeekOrigin.Begin);
  for (int sample = 0; sample < BufferSamples; sample++)
  {
    short amplitude = (short)(short.MaxValue * Math.Sin(angle)); 
    memoryStream.WriteByte((byte)(amplitude & 0xFF));
    memoryStream.WriteByte((byte)(amplitude >> 8));
    angle = (angle + angleIncrement) % (2 * Math.PI);
  }
  // Send out the sample
  ReportGetSampleCompleted(new MediaStreamSample(mediaStreamDescription,
    memoryStream,
    0,
    BufferSize,
    timestamp,
    mediaSampleAttributes));
  // Prepare for next sample
  timestamp += BufferSamples * 10000000L / sampleRate;
}

Die Amplitude wird dann in zwei Byte gesplittet und im MemoryStream mit dem niederstwertigen Byte zuerst gespeichert. Für den Stereoklang müssen für jedes Sample zwei 16-Bit-Werte vorhanden sein, die zwischen links und rechts wechseln.

Andere Berechnungen sind möglich. Sie können von einer Sinuswelle zu einer Sägezahnschwingung wechseln, indem Sie eine Amplitude wie diese definieren:

short amplitude = (short)(short.MaxValue * angle / Math.PI + short.MinValue);

In beiden Fällen umfasst eine Variable mit der Bezeichnung „angle“ (Winkel) einen Bereich von 0 bis 2 π Radiant (oder 360 Grad), verweist also auf einen einzelnen Zyklus mit einer spezifischen Waveform. Nach jedem Sample wird der Winkel anhand von angleIncrement erhöht, einer Variablen, die zuvor basierend auf der Frequenz der Samplerate und der Frequenz der zu erzeugenden Waveform in der Klasse berechnet wurde und hier mit 440 Hz hartcodiert ist:

angleIncrement = 2 * Math.PI * 440 / sampleRate;

Wie Sie sehen, erreicht angleIncrement den Wert π oder 180 Grad, sobald die Frequenz der generierten Waveform die Hälfte der Samplerate erreicht hat. Bei halber Samplerate basiert die erzeugte Waveform nur auf zwei Samples pro Zyklus; das Ergebnis ist eher eine Rechteckschwingung als eine sanfte Sinuskurve. Jedoch liegen alle harmonischen Schwingungen in dieser Rechteckschwingung über der halben Samplerate.

Wie Sie ebenfalls sehen, können Sie keine Frequenzen generieren, die über der halben Samplerate sind. Wenn Sie es versuchen, erzeugen Sie „Aliase“, die tatsächlich unter der halben Samplerate liegen. Diese halbe Samplerate wird als Nyquist-Frequenz bezeichnet. Sie ist nach dem Physiker Harry Nyquist benannt, der bei AT&T arbeitete und schon im Jahre 1928 eine Abhandlung über die Informationstheorie verfasste, die als Grundlage für die Sampling-Technologie im Audiobereich dient.

Für Audio-CDs wird eine Samplerate von 44.100 Hz verwendet, was teilweise daran liegt, dass auch die Hälfte von 44.100 Hz noch über dem menschlichen Hörvermögen liegt, das in der Regel mit 20.000 Hz beziffert wird.

Abgesehen von der Sine440AudioStreamSource-Klasse sind die anderen Elemente des SimpleAudioStreaming-Projekts recht einfach: Die Datei MainPage.xaml enthält ein MediaElement mit der Bezeichnung mediaElement, und die Überschreibung MainPage OnNavigatedTo ruft für dieses Objekt SetSource mit einer Instanz der MediaStreamSource-Ableitung auf:

mediaElement.SetSource(new Sine440AudioStreamSource(44100));

Ursprünglich hatte ich diesen Aufruf im Konstruktor des Programms festgelegt. Dann merkte ich, dass ich die Musik nicht wieder abspielen konnte, wenn ich aus dem Programm hinaus und wieder hinein navigierte, ohne dass ein Tombstoning des Programms erfolgt.

Die SetSource-Methode von MediaElement entspricht der von Ihnen aufgerufenen Methode, wenn Sie eine Musikdatei, auf die mit einem Streamobjekt verwiesen wird, mit MediaElement abspielen möchten. Durch den Aufruf von SetSource wird normalerweise die Soundwiedergabe gestartet, aber bei diesem MediaElement ist die AutoPlay-Eigenschaft auf „False“ gesetzt, deshalb ist für die Wiedergabe ein gesonderter Aufruf erforderlich. In der Anwendungsleiste des Programms sind Schaltflächen für Wiedergabe und Pause vorhanden, mit denen diese Vorgänge ausgeführt werden können.

Während das Programm ausgeführt wird, können Sie die Lautstärke über die universelle Lautstärkenregelung (UVC, Universal Volume Control) steuern. Wenn Sie allerdings das Programm beenden oder wegnavigieren, stoppt die Wiedergabe.

Verschieben in den Hintergrund

Dieselbe MediaStreamSource-Ableitung kann auch zur Wiedergabe von Sounds oder Musik im Hintergrund verwendet werden. Die Hintergrundmusik wird auch auf dem Telefon abgespielt, wenn Sie aus dem Programm navigieren oder dieses beenden. Bei Hintergrundaudio können Sie mithilfe der universellen Lautstärkeregelung des Telefons nicht nur die Lautstärke steuern, sondern zudem die Wiedergabe anhalten, wieder starten und (sofern möglich) nach vorne oder zurück zu anderen Tracks springen.

Sie werden sich freuen zu hören, dass vieles von dem, was in meinem Artikel in der letzten Monatsausgabe stand (msdn.microsoft.com/magazine/hh781030), auch für das Audiostreaming im Hintergrund gilt. In dem Artikel habe ich beschrieben, wie Musikdateien im Hintergrund abgespielt werden: Sie legen ein Bibliotheksprojekt an, das eine aus AudioPlayerAgent abgeleitete Klasse enthält. Fügen Sie ein neues Projekt des Typs Audiowiedergabe-Agent für Windows Phone hinzu, dann erzeugt Visual Studio diese Klasse für Sie.

Die Anwendung muss einen Verweis auf die DLL umfassen, die AudioPlayerAgent enthält, aber nicht direkt auf diese Klasse zugreifen. Stattdessen greift die Anwendung auf die BackgroundAudioPlayer-Klasse zu, um ein initiales AudioTrack-Objekt festzulegen und Wiedergabe und Pause aufzurufen. Sie erinnern sich, dass die AudioTrack-Klasse über einen Konstruktor verfügt, mit dem Sie den Tracktitel, den Künstler sowie den Albumnamen angeben können. Außerdem können Sie festlegen, welche Schaltflächen in der universellen Lautstärkeregelung verfügbar sein sollen.

In der Regel ist das erste Argument für den AudioTrack-Konstruktor ein Uri-Objekt, das die Quelle der Musikdatei angibt, die Sie abspielen möchten. Wenn diese AudioTrack-Instanz nun Audiostreaming ausführen soll, anstatt eine Musikdatei abzuspielen, setzen Sie dieses erste Konstruktorargument auf null. In dem Fall sucht BackgroundAudioPlayer in einer vom Programm referenzierten DLL nach einer aus AudioStreamingAgent abgeleiteten Klasse. In Visual Studio legen Sie eine solche DLL an, indem Sie ein Projekt des Typs Audiostreaming-Agent für Windows Phone hinzufügen.

Die im herunterladbaren Code für diesen Artikel enthaltene SimpleBackgroundAudioStreaming-Lösung zeigt, wie das funktioniert. Die Lösung umfasst das Anwendungsprojekt und zwei Bibliotheksprojekte; das erste heißt AudioPlaybackAgent und enthält eine AudioPlayer-Klasse, die aus AudioPlayerAgent abgeleitet wird, das zweite heißt AudioStreamAgent und enthält eine AudioTrackStreamer-Klasse, die aus AudioStreamingAgent abgeleitet wird. Das Anwendungsprojekt enthält Verweise auf beide Bibliotheksprojekte, versucht aber nicht, auf die tatsächlichen Klassen zuzugreifen. Aus Gründen, die ich bereits im vorigen Artikel dargelegt habe, ist das zwecklos.

Ich möchte erneut betonen, dass die Anwendung Verweise auf die DLLs der Hintergrund-Agents umfassen muss. Es ist einfach, diese Verweise auszulassen, da keine Fehler auftreten – aber die Musik wird nicht abgespielt.

Ein Großteil der Logik im SimpleBackgroundAudioStreaming-Projekt ist die gleiche wie in einem Programm, mit dem Musikdateien im Hintergrund abgespielt werden. Es gibt eine Ausnahme: Sobald BackgroundAudioPlayer versucht, einen Track abzuspielen, der ein Uri-Objekt mit dem Wert null aufweist, wird die OnBeginStreaming-Methode in der AudioStreamingAgent-Ableitung aufgerufen. Hier ist der beeindruckend einfache Weg für die Handhabung dieses Aufrufs:

protected override void OnBeginStreaming(AudioTrack track, AudioStreamer streamer)
{
  streamer.SetSource(new Sine440AudioStreamSource(44100));
}

Das war's! Das ist die gleiche Sine440AudioStreamSource-Klasse, die ich oben beschrieben habe, nun ist sie in das AudioStreamAgent-Projekt eingebunden.

Obwohl mit dem SimpleBackgroundAudioStreaming-Programm nur ein Track erzeugt wird, können Sie mit mehreren Track-Objekten arbeiten und AudioTrack-Objekte, die auf Musikdateien verweisen, mit solchen mischen, die Streaming nutzen. Beachten Sie, dass es sich beim AudioTrack-Objekt um ein Argument für die OnBeginStreaming-Überschreibung handelt. Sie können mit diesen Informationen also die bestimmte MediaStreamSource anpassen, die für diesen Track verwendet werden soll. Um weitere Informationen bereitzustellen, können Sie die Tag-Eigenschaft von AudioTrack nutzen. Sie kann auf eine beliebige Zeichenfolge festgelegt werden.

Erstellen eines Synthesizers

Eine konstante Schwingung bei 440 Hz kann ziemlich langweilig werden, also erstellen wir einen elektronischen Musiksynthesizer. Ich habe einen sehr rudimentären Synthesizer mit 12 Klassen und zwei Schnittstellen im Petzold.MusicSynthesis-Bibliotheksprojekt in der SynthesizerDemos-Lösung zusammengestellt. (Einige dieser Klassen entsprechen Code, den ich für Silverlight 3 geschrieben habe. Er erschien im Juli 2007 in meinem Blog, den Sie unter charlespetzold.com finden.)

Den Kern dieses Synthesizers bildet eine MediaStreamSource-Ableitung mit der Bezeichnung DynamicPcmStreamSource. Diese verfügt über eine SampleProvider-Eigenschaft, die folgendermaßen definiert ist:

public IStereoSampleProvider SampleProvider { get; set; }
The IStereoSampleProvider interface is simple:
public interface IStereoSampleProvider
{
  AudioSample GetNextSample();
}

AudioSample verfügt über zwei öffentliche Felder des short-Typs mit der Bezeichnung „Links“ und „Rechts“. In der GetSampleAsync-Methode ruft DynamicPcmStreamSource die GetNextSample-Methode auf, um ein Paar aus 16-Bit-Samples abzurufen:

AudioSample audioSample = SampleProvider.GetNextSample();

Die Mixer-Klasse implementiert IStereoSampleProvider. Mixer verfügt über eine Inputs-Eigenschaft, die wiederum aus einer Sammlung von Objekten des MixerInput-Typs besteht. Jedes MixerInput-Objekt weist eine folgendermaßen definierte Input-Eigenschaft des Typs IMonoSampleProvider auf:

public interface IMonoSampleProvider
{
  short GetNextSample();
}

Eine Klasse, die IMonoSampleProvider implementiert, trägt die Bezeichnung SteadyNoteDurationPlayer. Das ist eine abstrakte Klasse, die eine Reihe von Noten mit derselben Länge in einem bestimmten Tempo abspielen kann. Sie hat eine Oscillator-Eigenschaft, die ebenfalls IMonoSampleProvider implementiert, um die tatsächlichen Waveforms zu erzeugen. Aus SteadyNoteDurationPlayer werden zwei Klassen abgeleitet: Mit Sequencer wird eine Reihe von Noten wiederholt abgespielt, mit Rambler wird eine zufällige Notenfolge wiedergegeben. In der SynthesizerDemos-Lösung setze ich diese beiden Klassen in zwei verschiedenen Anwendungen ein. Die eine läuft nur im Vordergrund, die andere spielt Musik im Hintergrund ab.

Die Anwendung im Vordergrund trägt die Bezeichnung WaveformManipulator, sie bietet ein Steuerelement, mit dem Sie eine Waveform zur Wiedergabe der Musik interaktiv definieren können, wie in Abbildung 2 gezeigt.

The WaveformManipulator Program
Abbildung 2 Das WaveformManipulator-Programm

Die Punkte, mit denen die Waveform definiert wird, werden an eine Oscillator-Ableitung mit der Bezeichnung VariableWaveformOscillator übergeben. Wenn Sie die runden Berührungspunkte nach oben und nach unten bewegen, bemerken Sie eine Verzögerung von etwa einer Sekunde, bevor Sie die Veränderung in der musikalischen Klangfarbe tatsächlich hören. Das ist das Ergebnis der Standardpuffergröße von 1000 ms, die von MediaStreamSource festgelegt wird.

Das WaveformManipulator-Programm nutzt zwei mit denselben Noten, nämlich die Arpeggios e-Moll, a-Moll, d-Moll und G-Dur, geladene Sequencer-Objekte. Die Tempi für die beiden Sequencer-Objekte sind leicht unterschiedlich, sodass sie nicht synchron sind; zunächst entsteht ein Hall- oder Echo-Effekt, daraus folgt dann fast ein Kontrapunkt. (Das ist eine Form der „Prozessmusik“, die von den frühen Werken des amerikanischen Komponisten Steve Reich inspiriert ist.) Der Initialisierungscode von MainPage.xaml.cs „verdrahtet“ die Synthesizer-Komponenten miteinander, wie in Abbildung 3 dargestellt.

Abbildung 3 Synthesizer-Initialisierungscode im WaveformManipulator-Programm

// Initialize Waveformer control
for (int i = 0; i < waveformer.Points.Count; i++)
{
  double angle = (i + 1) * 2 * Math.PI / (waveformer.Points.Count + 1);
  waveformer.Points[i] = new Point(angle, Math.Sin(angle));
}
// Create two Sequencers with slightly different tempi
Sequencer sequencer1 = new Sequencer(SAMPLE_RATE)
{
  Oscillator = new VariableWaveformOscillator(SAMPLE_RATE)
  {
    Points = waveformer.Points
  },
  Tempo = 480
};
Sequencer sequencer2 = new Sequencer(SAMPLE_RATE)
{
  Oscillator = new VariableWaveformOscillator(SAMPLE_RATE)
  {
    Points = waveformer.Points
  },
  Tempo = 470
};
// Set the same Pitch objects in the Sequencer objects
Pitch[] pitches =
{
  ...
};
foreach (Pitch pitch in pitches)
{
  sequencer1.Pitches.Add(pitch);
  sequencer2.Pitches.Add(pitch);
}
// Create Mixer and MixerInput objects
mixer = new Mixer();
mixer.Inputs.Add(new MixerInput(sequencer1) { Space = -0.5 });
mixer.Inputs.Add(new MixerInput(sequencer2) { Space = 0.5 });

Die Objekte Mixer, DynamicPcmStreamSource und MediaElement werden in der OnNavigatedTo-Überschreibung miteinander verbunden:

DynamicPcmStreamSource dynamicPcmStreamSource =
  new DynamicPcmStreamSource(SAMPLE_RATE);
dynamicPcmStreamSource.SampleProvider = mixer;
mediaElement.SetSource(dynamicPcmStreamSource);

Da WaveformManipulator zum Abspielen der Musik MediaElement verwendet, erfolgt die Musikwiedergabe nur, wenn das Programm im Vordergrund ausgeführt wird. 

Einschränkungen bei der Hintergrundwiedergabe

Ich habe viel darüber nachgedacht, eine WaveformManipulator-Version zu entwerfen, mit der Musik im Hintergrund mithilfe von BackgroundAudioPlayer abgespielt werden kann. Offensichtlich kann die Waveform nur verändert werden, wenn das Programm im Vordergrund ist. Aber es gab ein Problem, das ich nicht lösen konnte. Ich habe es bereits im Artikel des letzten Monats angesprochen: Die DLLs des Hintergrund-Agents, die vom Programm für die Abwicklung der Hintergrundverarbeitung geliefert werden, werden in einer anderen Aufgabe ausgeführt als das Programm selbst. Die einzige Möglichkeit, die ich für den arbiträren Datenaustausch dieser beiden Aufgaben sehe, bestünde in isoliertem Speicher.

Ich habe mich entschieden, diesen Job nicht weiter zu verfolgen. Zum Teil lag das daran, dass ich eine bessere Idee für ein Programm mit Audiostreaming im Hintergrund hatte. Dabei handelte es sich um ein Programm, das eine beliebige Melodie abspielt, deren Noten sich aber leicht verändern würden, sobald der Beschleunigungsmesser eine Veränderung der Ausrichtung des Telefons registriert hat. Durch Schütteln des Telefons wäre eine gänzlich neue Melodie erzeugt worden.

Das Projekt gedieh bis zu meinem Versuch, dem AudioStreamAgent-Projekt einen Verweis auf eine Microsoft.Devices.Sensors-Assembly hinzuzufügen. Denn mir wurde ein Meldungsfeld mit einem roten X und dieser Mitteilung angezeigt: „Sie haben versucht, einen Verweis hinzuzufügen, der von keinem Hintergrund-Agent unterstützt wird.“ Offenbar können Hintergrund-Agents nicht mit dem Beschleunigungsmesser arbeiten. So viel zu diesem Programmkonzept!

Stattdessen schrieb ich ein Programm mit der Bezeichnung PentatonicRambler. Es spielt anhand von Streaming im Hintergrund eine immerwährende Melodie aus der pentatonischen Skala ab, die nur fünf schwarze Tasten des Klaviers umfasst. Die Noten werden von einer Synthesizer-Komponente namens Rambler zufällig ausgewählt. Jede Folgenote wird dabei entweder auf einen Schritt nach oben oder einen Schritt nach unten von der vorigen Note beschränkt. Weil große Tonsprünge fehlen, wirkt der daraus folgende Notenstrom mehr wie eine komponierte (oder improvisierte) als eine vollkommen zufällige Melodie.

In Abbildung 4 wird die OnBeginStreaming-Überschreibung in der AudioStreamingAgent-Ableitung dargestellt.

Abbildung 4 Das Synthesizer-Setup für „PentatonicRambler“

protected override void OnBeginStreaming(AudioTrack track, AudioStreamer streamer)
{
  // Create a Rambler
  Rambler rambler = new Rambler(SAMPLE_RATE,
  new Pitch(Note.Csharp, 4), // Start
  new Pitch(Note.Csharp, 2), // Minimum
  new Pitch(Note.Csharp, 6)) // Maximum
  {
    Oscillator = new AlmostSquareWave(SAMPLE_RATE),
    Tempo = 480
  };
  // Set allowable note values
  rambler.Notes.Add(Note.Csharp);
  rambler.Notes.Add(Note.Dsharp);
  rambler.Notes.Add(Note.Fsharp);
  rambler.Notes.Add(Note.Gsharp);
  rambler.Notes.Add(Note.Asharp);
  // Create Mixer and MixerInput objects
  Mixer mixer = new Mixer();
  mixer.Inputs.Add(new MixerInput(rambler));
  DynamicPcmStreamSource audioStreamSource =
    new DynamicPcmStreamSource(SAMPLE_RATE);
  audioStreamSource.SampleProvider = mixer;
  streamer.SetSource(audioStreamSource);
}

Ich hätte es vorgezogen, die gesammelten Synthesizer-Komponenten im Programm selbst zu definieren und dann dieses Setup an den Hintergrund-Agent zu übermitteln. Aber aufgrund der gegebenen Prozessisolation zwischen der Programmaufgabe und den Aufgaben des Hintergrund-Agents wäre das eine Menge Arbeit gewesen. Das Synthesizer-Setup hätte gänzlich als Textzeichenfolge (vielleicht XML-basiert) definiert und dann vom Programm über die Tag-Eigenschaft von AudioTrack an die AudioStreamingAgent-Ableitung übergeben werden müssen.

Inzwischen steht auf meiner Wunschliste für zukünftige Windows Phone-Verbesserungen auch die Möglichkeit, dass Programme mit den von ihnen aufgerufenen Hintergrund-Agents kommunizieren können.

Charles Petzold schreibt seit langem redaktionelle Beiträge für das MSDN Magazin. Die Adresse seiner Website lautet charlespetzold.com.

Unser Dank gilt den folgenden technischen Experten für das Lektorat dieses Artikels: Eric BieMark Hopkins und Chris Pearson