30. Januar 2017
Michael Hanslik
0

Native APIs in Universal-Apps nutzen – Klangerzeugung mit X-Audio2

Verschiedene Geräteklassen mit einer App zu erreichen ist mit Einführung der Universal Windows Plattform (UWP) für Windows10-Geräte sehr einfach geworden. Noch deckt diese Plattform aber nicht alle Funktionen ab, die sich Entwickler wünschen. Aus diesem Grund wird Universal-Apps auch der Zugriff auf verschiedene Win32- und COM-APIs, insbesondere aus dem Grafik- und Multimediabereich gestattet. Während für die Entwicklung von Universal-Apps verschiedene Sprachen zur Verfügung stehen, ist ein Zugriff auf diese Nativen APIs nur über C++ möglich.

Das dafür notwendige Vorgehen wird hier anhand einer App zur Soundgenerierung gezeigt. Die in C# geschrieben App soll dabei eine C++-Komponente ansteuern, welche einen einfachen Sinus-Ton erzeugt und mit Hilfe der XAudio2-API wiedergibt.

Komponenten des Projekts

Projekttypen

Mit VisualStudio 2015 und installiertem Windows10 SDK wird zuerst eine neue C#-Universal-App über das Template angelegt.

Template für die Universal App

Die Komponente zur Sounderzeugung soll als weiteres Projekt der Projektmappe hinzugefügt werden. Ziel ist eine wiederverwendbare Klassenbibliothek, diesmal mit der Sprache C++, genauer mit „C++ CX“, Microsofts Komponentenerweiterungen für Visual C++ zur Entwicklung auf der Windows Runtime Plattform.

Bei den Projektvorlagen wird es hier ein wenig unübersichtlich. Die einfache Win32-Klassenbibliothek ist nicht verwendbar, da diese das gesamte .NET-Framework nutzt und daher von der Universal-Apps nicht eingebunden werden kann. Die Projekttypen DLL (Universal Windows) bzw. Statische Bibliothek (Universal Windows) wären in einer C++ – Universal-App referenzierbar, nicht aber in einem ManagedCode-Projekt.

Somit bleibt nur eine Implementierung als „Windows-Runtime Komponente“, siehe Abbildung.

Template für die WindowsRuntimeComponent

Nach Erstellung sollte mach sicherstellen, dass der Schalter für die Kompilierung mit der CX-Erweiterung über das Flag „/ZW“ in den Projekteinstellungen aktiviert ist.

ZW-Flag

Windows Runtime Komponente im Detail

Damit die von einer Windows-Runtime Komponente veröffentlichten Objekte in allen Sprachen der UWP, also C++, C#, Visual Basic und JavaScript genutzt werden können, müssen diese als „Windows Runtime Typen“ implementiert werden. Für diese gelten einige Beschränkungen.

So dürfen für alle veröffentlichten Properties und Methoden nur Basistypen der Windows Runtime zum Einsatz kommen und nutzerdefinierte Typen müssen entweder als ref class / struct oder als value class / struct implementiert werden. Dabei werden Value-Typen in C# als Struktur, die Ref-Typen als Klasse zur Verfügung gestellt. Letztere müssen zusätzlich als „sealed“ markiert werden, können also nicht weiter abgeleitet werden. Daneben gelten weitere Einschränkungen, unter anderem zur Vererbung und Verwendung von Templates.

Nutzung von X-Audio2

Im Rahmen der Vorlage wurde für unsere Soundkomponente eine public ref class Class1 sealed generiert. Diese wird zunächst in AudioComponent umbenannt, dann kann mit der Implementierung des Headers AudioComponent.h begonnen werden:

#pragma once
#include <xaudio2.h>

#define PI 3.14159265358979323846f
#define SAMPLERATE 44100  
#define BUFFERSIZE 22500

Es wird eine Referenz auf die XAudio2-API hinzugefügt und es werden notwendige Konstanten definiert. Neben der Kreiszahl PI sind das die verwendete Samplerate in Samples pro Sekunde sowie die Größe des Puffers für die generierten Sounddaten in Byte. Bei einer Samplegröße von genau einem Byte ergibt das bei der definierten Abtastrate einen Puffer für eine halbe Sekunde Klanginformation.

Im Anschluss folgt die Beschreibung der Klasse AudioComponent:

public ref class AudioComponent sealed
{
  public:
    AudioComponent();
    void PlayTone();

  private:	
    BYTE* sounddata;

    IXAudio2* xaudio_engine;
    
    XAUDIO2_BUFFER xaudio_buffer;
    IXAudio2SourceVoice* xaudio_source; 
    IXAudio2MasteringVoice* xaudio_master;
};

Neben einem Konstruktor planen wir eine Methode PlayTone() zu Wiedergabe eines Klangs. Intern benötigen ein Bytearray für die generierte Klanginformation, eine Referenz auf die AudioEngine sowie  3 Objekte zur Organisation der Wiedergabe. Dazu sind unter XAudio2 mindestens eine Source-Voice mit dazugehörigem BUFFER-Objekt sowie eine Mastering-Voice notwendig. Diese mischt die Eingabedaten und übergibt sie der Windows Audio Session API (WASAPI):

X-Audio2 Process Chain

Implementierung der AudioComponent

Die Implementierung des Konstruktors in AudioComponent.cpp startet mit der Initialisierung der AudioEngine und der Mastering-Voice sowie der Initialisierung des XAUDIO2_BUFFER-Objekts. (Um den Code möglichst kurz zu halten, wird auf Auswertung sämtlicher Rückgabewerte verzichtet):

AudioComponent::AudioComponent()
{
  XAudio2Create(&xaudio_engine);
  xaudio_engine->CreateMasteringVoice(&xaudio_master);

  ZeroMemory(&xaudio_buffer, sizeof(xaudio_buffer));
  xaudio_buffer.AudioBytes = BUFFERSIZE;

Das verwendete Soundformat muss in der Struktur WAVEFORMATEX beschrieben werden:

  WAVEFORMATEX waveformat;
  waveformat.nChannels = 1;
  waveformat.nSamplesPerSec = SAMPLERATE;
  waveformat.wBitsPerSample = 8;
  waveformat.nAvgBytesPerSec = SAMPLERATE;
  waveformat.wFormatTag = WAVE_FORMAT_PCM;
  waveformat.nBlockAlign = 1;
  waveformat.cbSize = 0;

Wir beschreiben damit ein Mono-PCM-Format mit der definierten Samplerate und 8-Bit pro Sample. Diese Formatbeschreibung übergeben wir der Erstellungsmethode für die Source-Voice.

  xaudio_engine->CreateSourceVoice(&xaudio_source, &waveformat, 0, XAUDIO2_DEFAULT_FREQ_RATIO, NULL, NULL, NULL);
  xaudio_source->Start();

Die Source-Voice wurde bereits gestartet, noch liegen aber keinerlei Klangdaten vor. Diese generieren wir über eine Sinusmethode, deren Werte wir zu bestimmten Zeitpunkten ermitteln und in einen Bereich von 0-255 übertragen.

Erzeugung der Klangdaten

Die ermittelten Werte werden im Anschluss Byte für Byte in den Puffer gespeichert. Jedes Byte in diesem Puffer einspricht dann genau einem Sample für einen 440Hz-Sinuston. Abschließend müssen wir die generierte Klanginformation dem XAUDIO2_BUFFER-Objekt übergeben.

  sounddata = new BYTE[BUFFERSIZE];
  for (int i = 0; i < BUFFERSIZE; i++)
  {
    double v = sin(2 * PI * i * (440.0 / SAMPLERATE));
    sounddata[i] = (BYTE)(v * 128) + 128;
  }  
  
  xaudio_buffer.pAudioData = sounddata;
}

Da alle notwendigen Initialisierungen im Konstruktor vorgenommen wurden, reicht es in der Play-Methode das vorbereitete Buffer-Objekte der Source-Voice zu übergeben. In diesem Moment wird die Wiedergabe gestartet:

void AudioComponent::PlayTone()
{	
  xaudio_source->SubmitSourceBuffer(&xaudio_buffer);
}

Nutzung der Windows Runtime Komponente in der Universal App

Nach einem erfolgreichen Bauen des Projektes wechseln wir in unser App-Projekt und fügen hier eine Referenz unsere AudioComponent hinzu. In der Datei App.xaml.cs wird Instanzvariable unserer Audio-Komponente angelegt, VisualStudio sollte uns das Einbinden des entsprechenden Namespaces automatisch anbieten.

sealed partial class App : Application
{
    AudioComponent audioComponent;
    ...

Am Ende der OnLaunched()-Methode wird eine neue Instanz unserer Komponente angelegt, und die Wiedergabe direkt gestartet:

audioComponent = new AudioComponent();
audioComponent.PlayTone();

Sauberes Beenden

Bei einem Start der App sollte nun bereits ein Ton wiedergeben werden. Es fehlt aber noch ein „Herunterfahren“ unserer Sound-Komponente wenn die App nicht länger ausgeführt wird . Hierfür wird die Methode OnSuspending um einen Dispose()-Aufruf ergänzt:

var deferral = e.SuspendingOperation.GetDeferral();
audioComponent.Dispose();
deferral.Complete();

Bevor diese Zeile kompiliert, ergänzen wir unsere Audiokomponente um einen Destrukor. Dieser wird bei der Übertragung in die Windows-Runtime automatisch in eine Implementierung von IDisposable übersetzt, muss dafür aber zwingend als virtuell markiert werden. Ergänzt wird in  AudioComponent.h:

public:
    virtual ~AudioComponent();

Und die Implementierung in AudioComponent.cpp:

AudioComponent::~AudioComponent()
{
  xaudio_engine->Release();
  delete sounddata;
}

Debugging

Mit den Standardeinstellungen wird der C++-Code im Debugmodus zunächst  nicht angesprungen. Dafür muss in den Debug-Einstellungen der Universal-Apps der Debuggertyp auf „Gemischt“ gesetzt werden. Jetzt kann beispielsweise mittels Breakpoint überprüft werden, das der Dekonstruktor beim Beenden der App tatsächlich triggert.

Bereit für die Veröffentlichung?

Die UniversalApp nutzt nun erfolgreich X-Audio2 zum Abspielen eines einfachen Klangs. Ein Test mit dem App Certification Kit schlägt aber mit einer Reihe „unerlaubter Zugriffe“ fehl. Grund ist, dass nur die Debug-Versionen der nativen API nicht offiziell unterstützt werden. Für einen Release-Build bestätigt die Zertifizierung, dass keine unerlaubten Zugriffe erkannt werden, die App kann im Store veröffentlicht werden.

Das fertige Projekt kann hier heruntergeladen werden.

Mit ein paar Erweiterungen kann aus das Projekt zu einer kleinen Theremin-App ausgebaut werden. Dafür muss nur eine kontinuierliche Wiedergabe mittels Double-Buffering organisiert werden und aus der UI heraus muss eine Steuerung der Parameter zur Klangerzeugung möglich sein. Das fertige Beispiel findet sich hier.

Michael Hanslik ist Softwareentwickler im Microsoft-Umfeld mit Schwerpunkt im Bereich der UI- und Appentwicklung. Seit 2015 ist er für die Saxonia Systems AG in verschiedene Projekte im Bereich der Energiewirtschaft tätig. Sie erreichen ihn über michael.hanslik@saxsys.de

Xing 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0