Juni 2016

Band 31, Nummer 6

Moderne Apps – Experimente mit Audio auf der UWP

Von Frank La La

Die UWP (universelle Windows-Plattform) verfügt über erweiterte APIs zum Aufzeichnen von Audio und Video. Die Featuresammlung endet jedoch nicht bei Aufzeichnungsfunktionen. Mit nur wenigen Codezeilen können Entwickler Spezialeffekte auf Audiodaten in Echtzeit anwenden. Effekte wie etwa Hall und Echo sind in die API integriert und können recht einfach implementiert werden. In diesem Artikel untersuche ich einige Grundlagen der Audioaufzeichnung und der Anwendung von Spezialeffekten. Ich erstelle eine UWP-App, die Audiodaten aufzeichnen, speichern und verschiedene Filter und Spezialeffekte anwenden kann.

Einrichten des Projekts zum Aufzeichnen von Audiodaten

Für die Aufzeichnung von Audiodaten ist es erforderlich, dass die App berechtigt ist, auf das Mikrofon zuzugreifen. Dazu muss die Manifestdatei der App geändert werden. Doppelklicken Sie im Projektmappen-Explorer auf die Datei „Package.appxmanifest“. Diese Datei befindet sich immer im Stamm des Projekts.

Wenn das Editor-Fenster der Manifestdatei der App geöffnet wird, klicken Sie auf die Registerkarte „Funktionen“. Aktivieren Sie im Listenfeld „Funktionen“ die Funktion „Mikrofon“. Dies ermöglicht Ihrer App den Zugriff auf das Mikrofon des Endbenutzers. Ohne diese Änderung löst Ihre App eine Ausnahme aus, wenn Sie versuchen, auf das Mikrofon zuzugreifen.

Aufzeichnen von Audio

Bevor Sie mit dem Hinzufügen von Spezialeffekten zu Audiodaten beginnen, müssen Sie Audiodaten aufzeichnen können. Das ist recht einfach. Fügen Sie Ihrem Projekt zuerst eine Klasse hinzu, um den gesamten Audioaufzeichnungscode zu verkapseln. Nennen Sie diese Klasse „AudioRecorder“. Sie verwendet öffentliche Methoden zum Starten und Beenden der Aufzeichnung sowie zum Wiedergeben des soeben aufgezeichneten Audioclips. Zu diesem Zweck müssen Sie Ihrer Klasse einige Member hinzufügen. Der erste Member ist „MediaCapture“ mit Funktionen zum Aufzeichnen von Audio, Video und Bildern von einem Erfassungsgerät (z. B. einem Mikrofon oder einer Webcam):

private MediaCapture _mediaCapture;

Außerdem müssen Sie einen „InMemoryRandomAccessStream“ zum Erfassen der Eingabe aus dem Mikrofon im Arbeitsspeicher hinzufügen:

private InMemoryRandomAccessStream _memoryBuffer;

Damit Sie den Zustand Ihrer Aufzeichnung nachverfolgen können, fügen Sie Ihrer Klasse eine Eigenschaft „Boolean“ hinzu, auf die öffentlich zugegriffen werden kann:

public bool IsRecording { get; set; }

Für die Audioaufzeichnung ist eine Überprüfung erforderlich, ob bereits eine Aufzeichnung ausgeführt wird. Wenn dies der Fall ist, löst der Code eine Ausnahme aus. Andernfalls müssen Sie Ihren Arbeitsspeicher-Datenstrom initialisieren, die vorherige Aufzeichnungsdatei löschen und die Aufzeichnung beginnen.

Da die MediaCapture-Klasse mehrere Funktionen bereitstellt, müssen Sie angeben, dass Sie Audiodaten aufzeichnen möchten. Zu diesem Zweck erstellen Sie eine Instanz von „MediaCaptureInitializationSettings“. Der Code erstellt anschließend eine Instanz eines MediaCapture-Objekts und übergibt die „MediaCaptureInitializationSettings“ wie in Abbildung 1 gezeigt an die InitializeAsync-Methode.

Abbildung 1: Erstellen einer Instanz eines MediaCapture-Objekts

public async void Record()
  {
  if (IsRecording)
  {
    throw new InvalidOperationException("Recording already in progress!");
  }
  await Initialize();
  await DeleteExistingFile();
  MediaCaptureInitializationSettings settings =
    new MediaCaptureInitializationSettings
  {
    StreamingCaptureMode = StreamingCaptureMode.Audio
  };
  _mediaCapture = new MediaCapture();
  await _mediaCapture.InitializeAsync(settings);
  await _mediaCapture.StartRecordToStreamAsync(
    MediaEncodingProfile.CreateMp3(AudioEncodingQuality.Auto), _memoryBuffer);
  IsRecording = true;
}

Schließlich weisen Sie das MediaCapture-Objekt an, mit der Aufzeichnung zu beginnen, und übergeben Parameter, die angeben, dass die Aufzeichnung im MP3-Format erfolgt und wo die Daten gespeichert werden sollen.

Für das Beenden der Aufzeichnung sind wesentlich weniger Codezeilen erforderlich:

public async void StopRecording()
{
  await _mediaCapture.StopRecordAsync();
  IsRecording = false;
  SaveAudioToFile();
}

Die StopRecording-Methode führt drei Aktionen aus: Sie weist das Media­Capture-Objekt an, die Aufzeichnung zu beenden, legt den Aufzeichnungsstatus auf FALSE fest und speichert die Daten des Audiodatenstroms in einer MP3-Datei auf dem Datenträger.

Speichern von Audiodaten auf einem Datenträger

Sobald sich die erfassten Audiodaten im „InMemoryRandom­AccessStream“ befinden, möchten Sie die Inhalte auf einem Datenträger speichern. Abbildung 2 zeigt dies. Für das Speichern von Audiodaten aus einem In-Memory-Datenstrom müssen Sie die Inhalte in einen anderen Datenstrom kopieren und dann mithilfe von Push auf den Datenträger übertragen. Mithilfe der Hilfsprogramme im Namespace „Windows.ApplicationModel.Package“ können Sie den Pfad zum Installationsverzeichnis Ihrer App abrufen. (Während der Entwicklung ist dies das Verzeichnis „\bin\x86\Debug“ des Projekts.) In diesem Verzeichnis soll die Datei aufgezeichnet werden. Sie können den Code auf einfache Weise so ändern, dass ein anderer Speicherort verwendet wird oder der Benutzer den Speicherort auswählen kann.

Abbildung 2: Speichern von Audiodaten auf einem Datenträger

private async void SaveAudioToFile()
{
  IRandomAccessStream audioStream = _memoryBuffer.CloneStream();
  StorageFolder storageFolder = Package.Current.InstalledLocation;
  StorageFile storageFile = await storageFolder.CreateFileAsync(
    DEFAULT_AUDIO_FILENAME, CreationCollisionOption.GenerateUniqueName);
  this._fileName = storageFile.Name;
  using (IRandomAccessStream fileStream =
    await storageFile.OpenAsync(FileAccessMode.ReadWrite))
  {
    await RandomAccessStream.CopyAndCloseAsync(
      audioStream.GetInputStreamAt(0), fileStream.GetOutputStreamAt(0));
    await audioStream.FlushAsync();
    audioStream.Dispose();
  }
}

Wiedergeben von Audiodaten

Da ihre Audiodaten nun in einem In-Memory-Puffer und auf dem Datenträger vorhanden sind, sind zwei Wiedergabemöglichkeiten verfügbar: aus dem Arbeitsspeicher oder vom Datenträger.

Der Code zum Wiedergeben der Audiodaten aus dem Arbeitsspeicher ist recht einfach. Sie erstellen eine neue Instanz des MediaElement-Steuerelements, legen seine Quelle auf den In-Memory-Puffer fest, übergeben einen MIME-Typ an das Steuerelement und rufen dann die Methode „Play“ auf.

public void Play()
{
  MediaElement playbackMediaElement = new MediaElement();
  playbackMediaElement.SetSource(_memoryBuffer, "MP3");
  playbackMediaElement.Play();
}

Für die Wiedergabe vom Datenträger ist ein wenig mehr Code erforderlich, weil das Öffnen von Dateien ein asynchroner Task ist. Damit der UI-Thread mit einem Task kommunizieren kann, der für einen anderen Thread ausgeführt wird, müssen Sie „CoreDispatcher“ verwenden. „CoreDispatcher“ sendet Nachrichten zwischen dem Thread, für den ein bestimmter Codeteil ausgeführt wird, und dem UI-Thread. Auf diese Weise kann der Code den UI-Kontext aus einem anderen Thread abrufen. Eine hervorragende Beschreibung von „CoreDispatcher“ finden Sie in David Crooks Blogbeitrag zu diesem Thema unter bit.ly/1SbJ6up.

Abgesehen von den zusätzlichen Schritten zum Verarbeiten des asynchronen Codes ähnelt die Methode der zuvor beschriebenen Methode, die den In-Memory-Puffer verwendet:

public async Task PlayFromDisk(CoreDispatcher dispatcher)
{
  await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
  {
    MediaElement playbackMediaElement = new MediaElement();
    StorageFolder storageFolder = Package.Current.InstalledLocation;
    StorageFile storageFile = await storageFolder.GetFileAsync(this._fileName);
    IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.Read);
    playbackMediaElement.SetSource(stream, storageFile.FileType);
    playbackMediaElement.Play();
  });
}

Erstellen der Benutzeroberfläche

Da die AudioRecorder-Klasse nun vollständig ist, muss nur noch die Benutzeroberfläche für die App erstellt werden. Die Schnittstelle für dieses Projekt ist recht einfach, da nur eine Schaltfläche zum Aufzeichnen und eine Schaltfläche zum Wiedergeben der aufgezeichneten Audiodaten erforderlich ist. Abbildung 3 zeigt dies. Entsprechend einfach ist der XAML-Code: ein „TextBlock“ und ein StackPanel-Element mit zwei Schaltflächen:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid.RowDefinitions>
    <RowDefinition Height="43"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
<TextBlock FontSize="24">Audio in UWP</TextBlock>
<StackPanel HorizontalAlignment="Center" Grid.Row="1" >
  <Button Name="btnRecord" Click="btnRecord_Click">Record</Button>
  <Button Name="btnPlay" Click="btnPlay_Click">Play</Button>
</StackPanel>
</Grid>

AudioRecorder-Benutzeroberfläche
Abbildung 3: AudioRecorder-Benutzeroberfläche

In der CodeBehind-Klasse erstellen Sie eine Membervariable von „Audio­Recorder“. Dies ist das Objekt, das Ihre App zum Aufzeichnen und Wiedergeben von Audiodaten verwendet:

AudioRecorder _audioRecorder;

Sie instanziieren die AudioRecorder-Klasse im Konstruktor der „MainPage“ Ihrer App:

public MainPage()
{
  this.InitializeComponent();
  this._audioRecorder = new AudioRecorder();
}

Die Schaltfläche „btnRecord“ schaltet den Start und das Beenden der Audioaufzeichnung um. Damit der Benutzer über den aktuellen Zustand von „AudioRecorder“ informiert ist, ändert die btnRecord_Click-Methode den Inhalt der Schaltfläche „btnRecord“ und startet bzw. beendet die Aufzeichnung.

Für den Ereignishandler für die Schaltfläche „btnPlay“ sind zwei Optionen verfügbar: Wiedergabe aus dem In-Memory-Puffer oder Wiedergabe aus einer auf dem Datenträger gespeicherten Datei.

Für die Wiedergabe aus dem In-Memory-Puffer ist der Code recht einfach:

private void btnPlay_Click(object sender, RoutedEventArgs e)
{
  this._audioRecorder.Play();
}

Wie ich weiter oben bereits erwähnt habe, erfolgt die Wiedergabe der Datei vom Datenträger asynchron. Dies bedeutet, dass der Task für einen anderen Thread als der UI-Thread ausgeführt wird. Der Betriebssystemscheduler ermittelt zur Laufzeit, für welchen Thread der Task ausgeführt wird. Durch Übergeben des Dispatcher-Objekts an die PlayFromDisk-Methode kann der Thread Zugriff auf den UI-Kontext des UI-Threads erlangen:

private async void btnPlay_Click(object sender, RoutedEventArgs e)
{
  await this._audioRecorder.PlayFromDisk(Dispatcher);
}

Anwenden von Spezialeffekten

Da Ihre App nun Audiodaten aufzeichnen und wiedergeben kann, ist es an der Zeit, einige der weniger bekannten Features auf der UWP zu untersuchen: Audiospezialeffekte in Echtzeit. In den APIs sind im Namespace „Windows.Media.Audio“ mehrere Spezialeffekte enthalten, die Apps eine besondere Note verleihen können.

Für dieses Projekt platzieren Sie den gesamten Spezialeffektecode in einer eigenen Klasse. Bevor Sie die neue Klasse erstellen, nehmen Sie jedoch mindestens eine Änderung an der AudioRecorder-Klasse vor. Ich füge die folgende Methode hinzu:

public async Task<StorageFile>
   GetStorageFile(CoreDispatcher dispatcher)
{
  StorageFolder storageFolder =
    Package.Current.InstalledLocation;
  StorageFile storageFile =
    await storageFolder.GetFileAsync(this._fileName);
  return storageFile;
}

Die GetStorageFile-Methode gibt ein StorageFile-Objekt an die gespeicherte Audiodatei zurück. Auf diese Weise greift meine Spezialeffekteklasse auf die Audiodaten zu.

Einführung in AudioGraph

Die AudioGraph-Klasse ist für erweiterte Audioszenarien auf der UWP wesentlich. Ein AudioGraph kann Audiodaten von den Eingabequellknoten an Ausgabequellknoten über verschiedene Mixingknoten weiterleiten. Das ganze Ausmaß und die Leistungsfähigkeit von AudioGraph können in diesem Artikel nicht behandelt werden. Ich plane jedoch, in zukünftigen Artikeln tiefer in dieses Thema einzusteigen. Im Moment ist nur wichtig, dass auf jeden Knoten in einem AudioGraph mehrere Audioeffekte angewendet werden können. Weitere Informationen zu AudioGraph finden Sie im Artikel im Windows Dev Center unter bit.ly/1VCIBfD.

Im ersten Schritt fügen Sie Ihrem Projekt eine Klasse namens „AudioEffects“ sowie die folgenden Member hinzu:

private AudioGraph _audioGraph;
private AudioFileInputNode _fileInputNode;
private AudioDeviceOutputNode _deviceOutputNode;

Damit eine Instanz der AudioGraph-Klasse erstellt werden kann, müssen Sie ein AudioGraphSettings-Objekt erstellen, das die Konfigurationseinstellungen für AudioGraph enthält. Sie können dann die AudioGraph.Create­Async-Methode aufrufen und diese Konfigurationseinstellungen übergeben. Die CreateAsync-Methode gibt ein CreateAudioGraphResult-Objekt zurück. Diese Klasse stellt Zugriff auf den erstellten AudioGraph sowie einen Statuswert zur Verfügung, der angibt, ob die AudioGraph-Erstellung fehlerhaft oder erfolgreich war.

Sie müssen außerdem einen Ausgabeknoten für die Wiedergabe der Audiodaten erstellen. Rufen Sie zu diesem Zweck die CreateDevice­OutputNodeAsync-Methode für die AudioGraph-Klasse auf, und legen Sie die Membervariable auf die Eigenschaft „DeviceOutputNode“ des CreateAudioDeviceOutputNodeResult-Objekts fest. Der Code zum Initialisieren von AudioGraph und „Audio­DeviceOutputNode“ befindet sich vollständig hier in der InitializeAudioGraph-Methode:

public async Task InitializeAudioGraph()
{
  AudioGraphSettings settings = new AudioGraphSettings(AudioRenderCategory.Media);
  CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
  this._audioGraph = result.Graph;
  CreateAudioDeviceOutputNodeResult outputDeviceNodeResult =
    await this._audioGraph.CreateDeviceOutputNodeAsync();
  _deviceOutputNode = outputDeviceNodeResult.DeviceOutputNode;
}

Die Wiedergabe von Audiodaten aus einem AudioGraph-Objekt ist einfach: Rufen Sie nur die Play-Methode auf. Da AudioGraph ein privater Member Ihrer AudioEffects-Klasse ist, müssen Sie eine öffentliche Methode als Wrapper verwenden, um den Zugriff darauf zu ermöglichen:

public void Play()
{
this._audioGraph.Start();
}

Da Sie den Ausgabegerätknoten für Audio­Graph erstellt haben, müssen Sie einen Eingabeknoten aus der auf dem Datenträger gespeicherten Audiodatei erstellen. Sie müssen außerdem eine ausgehende Verbindung mit „FileInputNode“ hinzufügen. In diesem Fall soll der ausgehende Knoten Ihr Audioausgabegerät sein. So gehen Sie in der LoadFileIntoGraph-Methode vor:

public async Task LoadFileIntoGraph(StorageFile audioFile)
{
  CreateAudioFileInputNodeResult audioFileInputResult =
    await this._audioGraph.CreateFileInputNodeAsync(audioFile);
  _fileInputNode = audioFileInputResult.FileInputNode;
  _fileInputNode.AddOutgoingConnection(_deviceOutputNode);
  CreateAndAddEchoEffect();
}

Sie bemerken außerdem einen Verweis auf die CreateAndAddEchoEffect-Methode, die ich im nächsten Schritt behandeln werde.

Hinzufügen des Audioeffekts

Die AudioGraph-API enthält vier integrierte Audioeffekte: Echo, Hall, Equalizer und Limiter. In diesem Fall möchten Sie den aufgezeichneten Audiodaten ein Echo hinzufügen. Das Hinzufügen dieses Effekts ist ganz einfach: Sie erstellen das EchoEffectDefition-Objekt und legen dann die Eigenschaften des Effekts fest. Nach der Erstellung müssen Sie die Effektdefinition einem Knoten hinzufügen. In diesem Fall fügen Sie den Effekt „_fileInputNode“ hinzu. Dieses Objekt enthält die aufgezeichneten und auf dem Datenträger gespeicherten Audiodaten:

private void CreateAndAddEchoEffect()
{
  EchoEffectDefinition echoEffectDefinition = new EchoEffectDefinition(this._audioGraph);
  echoEffectDefinition.Delay = 100.0f;
  echoEffectDefinition.WetDryMix = 0.7f;
  echoEffectDefinition.Feedback = 0.5f;
  _fileInputNode.EffectDefinitions.Add(echoEffectDefinition);
}

Technologien kombinieren

Nachdem die AudioEffect-Klasse nun vollständig ist, können Sie sie über die Benutzeroberfläche verwenden. Zuerst fügen Sie der Hauptseite Ihrer App eine Schaltfläche hinzu:

<Button Content="Play with Special Effect" Click="btnSpecialEffectPlay_Click" />

Innerhalb des Klickereignishandlers rufen Sie die Datei ab, in der die Audiodaten gespeichert sind, erstellen eine Instanz der AudioEffects-Klasse und übergeben an diese die Datei mit den Audiodaten. Nachdem dies alles erfolgt ist, müssen Sie zum Wiedergeben der Audiodaten nur noch die Play-Methode aufrufen:

private async void btnSpecialEffectPlay_Click(object sender, RoutedEventArgs e)
{
  var storageFile = await this._audioRecorder.GetStorageFile(Dispatcher);
  AudioEffects effects = new AudioEffects();
  await effects.InitializeAudioGraph();
  await effects.LoadFileIntoGraph(storageFile);
  effects.Play();
}

Sie führen die App aus und klicken dann auf „Record“, um einen kleinen Audioclip aufzuzeichnen. Wenn Sie den Audioclip nach der Aufzeichnung wiedergeben möchten, klicken Sie auf die Schaltfläche „Play“. Wenn der gleiche Audioclip mit einem hinzugefügten Echo wiedergegeben werden soll, klicken Sie auf „Play with Special Effect“.

Zusammenfassung

Die UWP besitzt nicht nur erweiterte Unterstützung für das Erfassen von Audiodaten, sondern weist auch hervorragende Features zum Anwenden von Spezialeffekten auf Mediendateien in Echtzeit auf. Auf der Plattform sind verschiedene Effekte enthalten, die auf Audiodaten angewendet werden können. Es handelt sich beispielsweise um Echo, Hall, Equalizer und Limiter. Diese Effekte können einzeln oder in beliebigen Kombinationen angewendet werden. Die einzige Einschränkung ist Ihre Fantasie.


Frank La Vigneist ein IT-Experte im Microsoft Technology and Civic Engagement-Team. Sein Ziel ist es, Benutzern bei der Nutzung von IT-Technologie zu helfen, um für eine bessere Erfahrung zu sorgen. Auf FranksWorld.com führt er einen Blog, sein YouTube-Kanal heißt Frank’s World TV (youtube.com/FranksWorldTV).

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Drew Batchelor und Jose Luis Manners