Benutzerdefinierte Audioeffekte

Dieser Artikel beschreibt die Erstellung einer Windows Runtime-Komponente, die die Schnittstelle IBasicAudioEffect implementiert, um benutzerdefinierte Effekte für Audioströme zu erstellen. Benutzerdefinierte Effekte können mit verschiedenen Windows-Laufzeit-APIs verwendet werden, darunter MediaCapture, das den Zugriff auf die Kamera eines Geräts ermöglicht, MediaComposition, mit dem Sie komplexe Kompositionen aus Medienclips erstellen können, und AudioGraph , mit dem Sie schnell einen Graphen aus verschiedenen Audio-Eingangs-, Ausgangs- und Submix-Knoten zusammenstellen können.

Hinzufügen eines benutzerdefinierten Effekts zu Ihrer Anwendung

Ein benutzerdefinierter Audioeffekt wird in einer Klasse definiert, die die Schnittstelle IBasicAudioEffect implementiert. Diese Klasse kann nicht direkt in das Projekt Ihrer Anwendung eingebunden werden. Stattdessen müssen Sie eine Windows Runtime-Komponente verwenden, um Ihre Audioeffektklasse zu hosten.

Hinzufügen einer Windows-Laufzeitkomponente für Ihren Audioeffekt

  1. Gehen Sie in Microsoft Visual Studio bei geöffneter Lösung in das Menü Datei und wählen Sie Hinzufügen->Neues Projekt.
  2. Wählen Sie den Projekttyp Windows Runtime Component (Universal Windows).
  3. Für dieses Beispiel nennen Sie das Projekt AudioEffectComponent. Auf diesen Namen wird später im Code verwiesen.
  4. Klicken Sie auf OK.
  5. Die Projektvorlage erstellt eine Klasse namens Class1.cs. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Symbol für Class1.cs und wählen Sie Umbenennen.
  6. Benennen Sie die Datei in ExampleAudioEffect.cs um. Visual Studio zeigt eine Eingabeaufforderung an, in der Sie gefragt werden, ob Sie alle Verweise auf den neuen Namen aktualisieren möchten. Klicken Sie auf Ja.
  7. Öffnen Sie ExampleAudioEffect.cs und aktualisieren Sie die Klassendefinition, um die Schnittstelle IBasicAudioEffect zu implementieren.
public sealed class ExampleAudioEffect : IBasicAudioEffect

Sie müssen die folgenden Namespaces in Ihre Effektklassendatei aufnehmen, um auf alle in den Beispielen dieses Artikels verwendeten Typen zugreifen zu können.

using Windows.Media.Effects;
using Windows.Media.MediaProperties;
using Windows.Foundation.Collections;
using System.Runtime.InteropServices;
using Windows.Media;
using Windows.Foundation;

Implementierung der Schnittstelle IBasicAudioEffect

Ihr Audioeffekt muss alle Methoden und Eigenschaften der Schnittstelle IBasicAudioEffect implementieren. Dieser Abschnitt führt Sie durch eine einfache Implementierung dieser Schnittstelle, um einen einfachen Echoeffekt zu erzeugen.

SupportedEncodingProperties-Eigenschaft

Das System überprüft die Eigenschaft SupportedEncodingProperties, um festzustellen, welche Kodierungseigenschaften von Ihrem Effekt unterstützt werden. Beachten Sie, dass das System Close für Ihren Effekt aufruft und Ihren Effekt aus der Audiopipeline entfernt, wenn der Verbraucher Ihres Effekts kein Audio mit den von Ihnen angegebenen Eigenschaften kodieren kann. In diesem Beispiel werden die Objekte AudioEncodingProperties erstellt und der zurückgegebenen Liste hinzugefügt, um 44,1 kHz und 48 kHz, 32-Bit-Float, Mono-Codierung zu unterstützen.

public IReadOnlyList<AudioEncodingProperties> SupportedEncodingProperties
{
    get
    {
        var supportedEncodingProperties = new List<AudioEncodingProperties>();
        AudioEncodingProperties encodingProps1 = AudioEncodingProperties.CreatePcm(44100, 1, 32);
        encodingProps1.Subtype = MediaEncodingSubtypes.Float;
        AudioEncodingProperties encodingProps2 = AudioEncodingProperties.CreatePcm(48000, 1, 32);
        encodingProps2.Subtype = MediaEncodingSubtypes.Float;

        supportedEncodingProperties.Add(encodingProps1);
        supportedEncodingProperties.Add(encodingProps2);

        return supportedEncodingProperties;
        
    }
}

SetEncodingProperties-Methode

Das System ruft SetEncodingProperties für Ihren Effekt auf, um Ihnen die Codierungseigenschaften für den Audiostrom mitzuteilen, mit dem der Effekt arbeitet. Um einen Echo-Effekt zu implementieren, wird in diesem Beispiel ein Puffer verwendet, in dem eine Sekunde an Audiodaten gespeichert wird. Diese Methode bietet die Möglichkeit, die Größe des Puffers auf die Anzahl der Samples in einer Sekunde Audio zu initialisieren, basierend auf der Samplerate, in der das Audio kodiert ist. Der Verzögerungseffekt verwendet auch einen Integer-Zähler, um die aktuelle Position im Verzögerungspuffer zu verfolgen. Da SetEncodingProperties immer dann aufgerufen wird, wenn der Effekt zur Audiopipeline hinzugefügt wird, ist dies ein guter Zeitpunkt, um diesen Wert auf 0 zu initialisieren. Sie können auch das Objekt AudioEncodingProperties erfassen, das an diese Methode übergeben wird, um es an anderer Stelle in Ihrem Effekt zu verwenden.

private float[] echoBuffer;
private int currentActiveSampleIndex;
private AudioEncodingProperties currentEncodingProperties;
public void SetEncodingProperties(AudioEncodingProperties encodingProperties)
{
    currentEncodingProperties = encodingProperties;
    echoBuffer = new float[encodingProperties.SampleRate]; // exactly one second delay
    currentActiveSampleIndex = 0;
}

SetProperties-Methode

Die Methode SetProperties ermöglicht es der Anwendung, die Ihren Effekt verwendet, die Effektparameter anzupassen. Eigenschaften werden als IPropertySet Karte der Eigenschaftsnamen und -werte übergeben.

IPropertySet configuration;
public void SetProperties(IPropertySet configuration)
{
    this.configuration = configuration;
}

In diesem einfachen Beispiel wird das aktuelle Audiosample mit einem Wert aus dem Verzögerungspuffer gemäß dem Wert der Eigenschaft Mix gemischt. Eine Eigenschaft wird deklariert und TryGetValue wird verwendet, um den von der aufrufenden Anwendung gesetzten Wert zu erhalten. Wurde kein Wert festgelegt, wird ein Standardwert von 0,5 verwendet. Beachten Sie, dass diese Eigenschaft schreibgeschützt ist. Der Eigenschaftswert muss mit SetProperties gesetzt werden.

public float Mix
{
    get
    {
        object val;
        if (configuration != null && configuration.TryGetValue("Mix", out val))
        {
            return (float)val;
        }
        return .5f;
    }
}

ProcessFrame-Methode

Die Methode ProcessFrame ist die Methode, mit der Ihr Effekt die Audiodaten des Streams modifiziert. Die Methode wird einmal pro Frame aufgerufen und bekommt ein ProcessAudioFrameContext-Objekt übergeben. Dieses Objekt enthält ein input AudioFrame-Objekt, das den eingehenden Frame enthält, der verarbeitet werden soll, und ein output AudioFrame-Objekt, in das Sie Audiodaten schreiben, die an den Rest der Audio-Pipeline weitergegeben werden. Ein Audio-Frame ist ein Puffer von Audio-Samples, der einen kurzen Audiodaten-Slice darstellt.

Der Zugriff auf den Datenpuffer eines AudioFrame erfordert COM-Interop, daher sollten Sie den Namespace System.Runtime.InteropServices in Ihre Effektklassendatei aufnehmen und dann den folgenden Code innerhalb des Namespaces für Ihren Effekt hinzufügen, um die Schnittstelle für den Zugriff auf den Audiopuffer zu importieren.

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

Hinweis

Da diese Technik auf einen nativen, nicht verwalteten Bildpuffer zugreift, müssen Sie Ihr Projekt so konfigurieren, dass unsicherer Code zugelassen wird.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt AudioEffectComponent und wählen Sie Eigenschaften.
  2. Wählen Sie die Registerkarte Erstellen aus.
  3. Aktivieren Sie das Kontrollkästchen Unsicheren Code zulassen.

 

Jetzt können Sie Ihrem Effekt die Implementierung der Methode ProcessFrame hinzufügen. Zunächst erhält diese Methode ein AudioBuffer-Objekt aus den Eingangs- und Ausgangs-Audio-Frames. Beachten Sie, dass der Ausgangsrahmen zum Schreiben und der Eingangsrahmen zum Lesen geöffnet ist. Als nächstes wird eine IMemoryBufferReference für jeden Puffer durch Aufruf von CreateReference erstellt. Dann wird der tatsächliche Datenpuffer durch Casting der Objekte IMemoryBufferReference als die oben definierte COM-Interop-Schnittstelle IMemoryByteAccess erhalten und dann GetBuffer aufgerufen.

Nachdem Sie die Datenpuffer erhalten haben, können Sie nun aus dem Eingangspuffer lesen und in den Ausgangspuffer schreiben. Für jedes Sample im Eingangspuffer wird der Wert ermittelt und mit 1 - Mix multipliziert, um den trockenen Signalwert des Effekts einzustellen. Als nächstes wird ein Sample von der aktuellen Position im Echopuffer abgerufen und mit Mix multipliziert, um den Wet-Wert des Effekts einzustellen. Die Ausgangsprobe wird auf die Summe der Trocken- und Nasswerte eingestellt. Abschließend wird jedes Eingangssample im Echopuffer gespeichert und der aktuelle Sample-Index inkrementiert.

unsafe public void ProcessFrame(ProcessAudioFrameContext context)
{
    AudioFrame inputFrame = context.InputFrame;
    AudioFrame outputFrame = context.OutputFrame;

    using (AudioBuffer inputBuffer = inputFrame.LockBuffer(AudioBufferAccessMode.Read),
                        outputBuffer = outputFrame.LockBuffer(AudioBufferAccessMode.Write))
    using (IMemoryBufferReference inputReference = inputBuffer.CreateReference(),
                                    outputReference = outputBuffer.CreateReference())
    {
        byte* inputDataInBytes;
        byte* outputDataInBytes;
        uint inputCapacity;
        uint outputCapacity;

        ((IMemoryBufferByteAccess)inputReference).GetBuffer(out inputDataInBytes, out inputCapacity);
        ((IMemoryBufferByteAccess)outputReference).GetBuffer(out outputDataInBytes, out outputCapacity);

        float* inputDataInFloat = (float*)inputDataInBytes;
        float* outputDataInFloat = (float*)outputDataInBytes;

        float inputData;
        float echoData;

        // Process audio data
        int dataInFloatLength = (int)inputBuffer.Length / sizeof(float);

        for (int i = 0; i < dataInFloatLength; i++)
        {
            inputData = inputDataInFloat[i] * (1.0f - this.Mix);
            echoData = echoBuffer[currentActiveSampleIndex] * this.Mix;
            outputDataInFloat[i] = inputData + echoData;
            echoBuffer[currentActiveSampleIndex] = inputDataInFloat[i];
            currentActiveSampleIndex++;

            if (currentActiveSampleIndex == echoBuffer.Length)
            {
                // Wrap around (after one second of samples)
                currentActiveSampleIndex = 0;
            }
        }
    }
}

Close-Methode

Das System ruft die Methode CloseClose in Ihrer Klasse auf, wenn der Effekt beendet werden soll. Sie sollten diese Methode verwenden, um alle Ressourcen zu entsorgen, die Sie erstellt haben. Das Argument der Methode ist ein MediaEffectClosedReason, das Ihnen mitteilt, ob der Effekt normal geschlossen wurde, ob ein Fehler aufgetreten ist oder ob der Effekt das erforderliche Codierungsformat nicht unterstützt.

public void Close(MediaEffectClosedReason reason)
{
    // Dispose of effect resources
    echoBuffer = null;
}

DiscardQueuedFrames-Methode

Die Methode DiscardQueuedFrames wird aufgerufen, wenn Ihr Effekt zurückgesetzt werden soll. Ein typisches Szenario hierfür ist, dass Ihr Effekt zuvor verarbeitete Frames speichert, um sie für die Verarbeitung des aktuellen Frames zu verwenden. Wenn diese Methode aufgerufen wird, sollten Sie den Satz der zuvor gespeicherten Bilder entsorgen. Mit dieser Methode kann jeder Zustand zurückgesetzt werden, der sich auf frühere Frames bezieht, nicht nur auf akkumulierte Audio-Frames.

public void DiscardQueuedFrames()
{
    // Reset contents of the samples buffer
    Array.Clear(echoBuffer, 0, echoBuffer.Length - 1);
    currentActiveSampleIndex = 0;
}

TimeIndependent-Eigenschaft

Die Eigenschaft TimeIndependent TimeIndependent lässt das System wissen, wenn Ihr Effekt kein einheitliches Timing erfordert. Wenn diese Option auf true gesetzt ist, kann das System Optimierungen verwenden, die die Effektleistung verbessern.

public bool TimeIndependent { get { return true; } }

UseInputFrameForOutput-Eigenschaft

Setzen Sie die Eigenschaft UseInputFrameForOutput auf true, um dem System mitzuteilen, dass Ihr Effekt seine Ausgabe in den Audiopuffer des InputFrame des ProcessAudioFrameContext übergebenen ProcessFrame schreibt, anstatt in den OutputFrame.

public bool UseInputFrameForOutput { get { return false; } }

Hinzufügen Ihres benutzerdefinierten Effekts zu Ihrer Anwendung

Um Ihren Audioeffekt in Ihrer Anwendung zu verwenden, müssen Sie einen Verweis auf das Effektprojekt zu Ihrer Anwendung hinzufügen.

  1. Klicken Sie im Projektmappen-Explorer unter Ihrem Anwendungsprojekt mit der rechten Maustaste auf Referenzen und wählen Sie Referenz hinzufügen.
  2. Erweitern Sie die Registerkarte Projekte, wählen Sie Lösung und aktivieren Sie dann das Kontrollkästchen für den Namen Ihres Effektprojekts. In diesem Beispiel lautet der Name AudioEffectComponent.
  3. Klicken Sie auf OK.

Wenn Ihre Audioeffektklasse in einem anderen Namespace deklariert ist, müssen Sie diesen Namespace in Ihre Codedatei aufnehmen.

using AudioEffectComponent;

Hinzufügen eines benutzerdefinierten Effekts zu einem AudioGraph-Knoten

Allgemeine Informationen zur Verwendung von Audiographen finden Sie unter Audiographen. Das folgende Codeschnipsel zeigt Ihnen, wie Sie den in diesem Artikel gezeigten Beispiel-Echo-Effekt zu einem Audio-Graph-Knoten hinzufügen können. Zunächst wird ein PropertySet erstellt und ein Wert für die Eigenschaft Mix festgelegt, der durch den Effekt definiert wird. Als Nächstes wird der Konstruktor AudioEffectDefinition aufgerufen, wobei der vollständige Klassenname des benutzerdefinierten Effekttyps und der Eigenschaftssatz übergeben werden. Schließlich wird die Effektdefinition zur Eigenschaft EffectDefinitions eines vorhandenen FileInputNode hinzugefügt, so dass die ausgegebenen Audiodaten durch den benutzerdefinierten Effekt verarbeitet werden.

// Create a property set and add a property/value pair
PropertySet echoProperties = new PropertySet();
echoProperties.Add("Mix", 0.5f);

// Instantiate the custom effect defined in the 'AudioEffectComponent' project
AudioEffectDefinition echoEffectDefinition = new AudioEffectDefinition(typeof(ExampleAudioEffect).FullName, echoProperties);
fileInputNode.EffectDefinitions.Add(echoEffectDefinition);

Nachdem er zu einem Knoten hinzugefügt wurde, kann der benutzerdefinierte Effekt deaktiviert werden, indem DisableEffectsByDefinition aufgerufen und das Objekt AudioEffectDefinition übergeben wird. Weitere Informationen zur Verwendung von Audiographen in Ihrer Anwendung finden Sie unter AudioGraph.

Hinzufügen eines benutzerdefinierten Effekts zu einem Clip in einer MediaComposition

Der folgende Codeausschnitt veranschaulicht das Hinzufügen des benutzerdefinierten Audioeffekts zu einem Videoclip und einer Hintergrund-Audiospur in einer Medienkomposition. Eine allgemeine Anleitung zum Erstellen von Medienkompositionen aus Videoclips und zum Hinzufügen von Hintergrund-Audiospuren finden Sie unter Medienkompositionen und Bearbeitung.

// Create a property set and add a property/value pair
PropertySet echoProperties = new PropertySet();
echoProperties.Add("Mix", 0.5f);

// Instantiate the custom effect defined in the 'AudioEffectComponent' project
AudioEffectDefinition echoEffectDefinition = new AudioEffectDefinition(typeof(ExampleAudioEffect).FullName, echoProperties);

// Add custom audio effect to the current clip in the timeline
var currentClip = composition.Clips.FirstOrDefault(
    mc => mc.StartTimeInComposition <= mediaPlayerElement.MediaPlayer.PlaybackSession.Position &&
    mc.EndTimeInComposition >= mediaPlayerElement.MediaPlayer.PlaybackSession.Position);
currentClip.AudioEffectDefinitions.Add(echoEffectDefinition);

// Add custom audio effect to the first background audio track
if (composition.BackgroundAudioTracks.Count > 0)
{
    composition.BackgroundAudioTracks[0].AudioEffectDefinitions.Add(echoEffectDefinition);
}