自訂音訊效果

本文介紹如何建立實作 IBasicAudioEffect 介面的 Windows 執行階段元件,為音訊串流建立自訂效果。 自訂效果可與多種不同的 Windows 執行階段 API 一起使用,包括 MediaCapture (提供對裝置相機的存取)、MediaComposition (可讓您從媒體短片建立複雜的組合) 以及 AudioGraph (讓您快速組合各種音訊輸入的圖表)、輸出和子混合節點。

在您的應用程式中新增自訂效果

自訂音訊效果定義於實作 IBasicAudioEffect 介面的類別中。 此類別無法直接包含在您應用程式的專案中。 相反地,您必須使用 Windows 執行階段元件來裝載音訊效果類別。

為您的音訊效果新增 Windows 執行階段元件

  1. 在 Microsoft Visual Studio 中,開啟解決方案後,請前往檔案功能表並選取新增->新專案
  2. 選取 Windows 執行階段元件 (通用 Windows) 專案類型。
  3. 在此範例中,將專案命名為 AudioEffectComponent。 稍後會在程式碼中參考此名稱。
  4. 按一下 [確定]
  5. 專案範本會建立名為 Class1.cs 的類別。 在方案總管中,以滑鼠右鍵按一下 Class1.cs 圖示,然後選取重新命名
  6. 將檔案重新命名為 ExampleAudioEffect.cs。 Visual Studio 會顯示提示,詢問您是否要更新新名稱的所有參考。 按一下
  7. 開啟 ExampleAudioEffect.cs 並更新類別定義以實作 IBasicAudioEffect 介面。
public sealed class ExampleAudioEffect : IBasicAudioEffect

您必須在效果類別檔案中包含下列命名空間,才能存取本文範例中使用的所有類型。

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

實作 IBasicAudioEffect 介面

您的音訊效果必須實作 IBasicAudioEffect 介面的所有方法和屬性。 本節將逐步引導您完成這個介面的簡單實作,以建立基本的迴聲效果。

SupportedEncodingProperties 屬性

系統會檢查 SupportedEncodingProperties 屬性,以判斷您的效果支援哪些編碼屬性。 請注意,如果效果的取用者無法使用您指定的屬性來編碼音訊,系統將會在效果上呼叫 Close,並將您的效果從音訊管線中移除。 在此範例中,AudioEncodingProperties 物件會建立並新增至傳回的清單,以支援 44.1 kHz 和 48 kHz、32 位元浮點數、單聲道編碼。

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 方法

系統會在效果上呼叫 SetEncodingProperties,讓您知道效果運作所在的音訊串流編碼屬性。 為了實作迴聲效果,這個範例會使用緩衝區來儲存一秒的音訊資料。 這個方法提供機會,根據音訊編碼的取樣速率,將緩衝區的大小初始化為一秒音訊中的樣本數目。 延遲效果也會使用整數計數器來追蹤延遲緩衝區中的目前位置。 由於每當將效果新增至音訊管線時,就會呼叫 SetEncodingProperties,因此這是將該值初始化為 0 的好時機。 您也可以擷取傳入此方法的 AudioEncodingProperties 物件,以在效果中其他地方使用。

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 方法

SetProperties 方法允許使用您的效果的應用程式調整效果參數。 屬性做為屬性名稱和值的 IPropertySet 對應進行傳遞。

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

這個簡單的範例將根據 Mix 屬性的值將目前音訊樣本與延遲緩衝區中的值混合。 屬性會宣告,而 TryGetValue 是用來取得呼叫應用程式所設定的值。 如果未設定任何值,則會使用預設值 .5。 請注意,此屬性是唯讀的。 屬性值必須使用 SetProperties 來設定。

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

ProcessFrame 方法

ProcessFrame 方法是您的效果修改串流的音訊資料的位置。 此方法每個畫面呼叫一次,並傳遞一個 ProcessAudioFrameContext 物件。 該物件包含一個輸入 AudioFrame 物件 (其中包含要處理的傳入畫面) 和一個輸出 AudioFrame 物件,您可以在其中寫入音訊資料,這些資料將傳遞到音訊管線的其餘部分。 音訊畫面是音訊樣本的緩衝區,代表音訊資料的簡短配量。

存取 AudioFrame 的資料緩衝區需要 COM Interop,因此您應該在效果類別檔案中包含 System.Runtime.InteropServices 命名空間,然後在命名空間內新增以下程式碼,以便您的效果匯入用於存取音訊緩衝區的介面。

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

注意

由於這項技術會存取原生非受控影像緩衝區,因此您必須設定專案以允許不安全的程式碼。

  1. 在方案總管中,以滑鼠右鍵按一下 AudioEffectComponent 專案並選取 屬性
  2. 選取組建索引標籤。
  3. 選取允許不安全程式碼核取方塊。

 

現在您可以將 ProcessFrame 方法實作新增到您的效果中。 首先,此方法從輸入和輸出音訊畫面中取得 AudioBuffer 物件。 請注意,輸出畫面已開啟以供寫入,以及讀取的輸入。 接下來,透過呼叫 CreateReference 為每個緩衝區取得 IMemoryBufferReference。 然後,透過將 IMemoryBufferReference 物件轉換為上面定義的 COM Interop 介面 IMemoryByteAccess,然後呼叫 GetBuffer 來取得實際的資料緩衝區。

現在已取得資料緩衝區,您可以從輸入緩衝區讀取並寫入輸出緩衝區。 針對 inputbuffer 中的每個樣本,會取得值並乘以 1 - Mix 來設定效果的乾燥訊號值。 接下來,從迴聲緩衝區中的目前位置擷取樣本並乘以 Mix 以設定效果的濕潤值。 輸出樣本會設定為乾燥和濕潤值的總和。 最後,每個輸入樣本都會儲存在迴聲緩衝區中,而目前的範例索引會遞增。

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 方法

當效果應該關閉時,系統將呼叫類別上的 CloseClose 方法。 您應該使用這個方法來處置您已建立的任何資源。 此方法的引數是 MediaEffectClosedReason,它可以讓您知道效果是否正常關閉、是否發生錯誤或效果是否不支援所需的編碼格式。

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

DiscardQueuedFrames 方法

當您的效果應重設時,將會呼叫 DiscardQueuedFrames 方法。 一個典型的案例是,您的效果會儲存先前處理的畫面,以用於處理目前的畫面。 呼叫此方法時,您應該處置先前儲存的畫面集。 這個方法可以用來重設與先前畫面相關的任何狀態,而不只是累積的音訊畫面。

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

TimeIndependent 屬性

TimeIndependent TimeIndependent 屬性讓系統知道您的效果是否不需要統一計時。 當設定為 true 時,系統可以使用可增強效果效能的最佳化。

public bool TimeIndependent { get { return true; } }

UseInputFrameForOutput 屬性

UseInputFrameForOutput 屬性設為 true 以告訴系統您的效果會將其輸出寫入傳入到 ProcessFrameProcessAudioFrameContextInputFrame 的音訊緩衝區,而不是寫入 OutputFrame

public bool UseInputFrameForOutput { get { return false; } }

將自訂效果新增至您的應用程式

若要從您的應用程式使用音訊效果,您必須將效果項目的參考新增至您的應用程式。

  1. 在方案總管中,於您的應用程式專案下,以滑鼠右鍵按一下參考,然後選取新增參考
  2. 展開專案索引標籤,選取解決方案,然後選取效果項目名稱的核取方塊。 在此範例中,名稱為 AudioEffectComponent
  3. 按一下 [檔案] > [新增] > [專案]

如果您的音訊效果類別宣告為不同的命名空間,請務必在程式碼檔案中包含該命名空間。

using AudioEffectComponent;

將自訂效果新增至 AudioGraph 節點

有關使用音訊圖的一般資訊,請參閱音訊圖。 下列程式碼片段示範如何將本文所示的範例迴聲效果新增至音訊圖節點。 首先,建立一個 PropertySet,並設定由效果定義的 Mix 屬性的值。 接下來,呼叫 AudioEffectDefinition 建構函數,傳入自訂效果類型的完整類別名稱和屬性集。 最後,將效果定義新增至現有 FileInputNodeEffectDefinitions 屬性中,使發出的音訊由自訂效果處理。

// 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);

將其新增至節點後,可以透過呼叫 DisableEffectsByDefinition 並傳入 AudioEffectDefinition 物件來停用自訂效果。 如需在應用程式中使用音訊圖的詳細資訊,請參閱 AudioGraph

將自訂效果新增至 MediaComposition 中的剪輯

下列程式碼片段示範如何在媒體組合中將自訂音訊效果新增至影片剪輯和背景曲目。 有關從影片剪輯建立媒體組合和新增背景音軌的一般指南,請參閱媒體組合和編輯

// 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);
}