カスタムのオーディオ特殊効果

この記事では、オーディオ ストリームのカスタム効果を作成するための IBasicAudioEffect インターフェイスを実装する Windows ランタイム コンポーネントを作成する方法について説明します。 カスタム効果は、デバイスのカメラへのアクセスを提供する MediaCapture、メディア クリップから複雑なコンポジションを作成するための MediaComposition、さまざまなオーディオ入力、出力、サブミックス ノードのグラフをすばやくアセンブルできる AudioGraph など、さまざまな Windows ランタイム API で使用できます。

アプリへのカスタム効果の追加

カスタムのオーディオ効果は、IBasicAudioEffect インターフェイスを実装するクラスで定義します。 このクラスは、アプリのプロジェクトに直接含めることはできません。 代わりに、Windows ランタイム コンポーネントを使ってオーディオ特殊効果のクラスをホストする必要があります。

オーディオ特殊効果用の Windows ランタイム コンポーネントの追加

  1. Microsoft Visual Studio で、ソリューションを開き、[ファイル] メニューから [追加] -> [新しいプロジェクト] の順に選択します。
  2. プロジェクトの種類として [Windows ランタイム コンポーネント (ユニバーサル Windows)] を選びます。
  3. この例では、プロジェクトに AudioEffectComponent という名前を付けます。 この名前は後でコードで参照されます。
  4. [OK] をクリックします。
  5. プロジェクト テンプレートに基づいて、Class1.cs という名前のクラスが作成されます。 ソリューション エクスプローラーで、Class1.cs のアイコンを右クリックし、[名前の変更] をクリックします。
  6. ファイル名を ExampleAudioEffect.cs に変更します。 すべての参照を新しい名前に更新するかどうかを確認するメッセージが表示されたら、 [はい] をクリックします。
  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 は、効果の対象となるオーディオ ストリームのエンコード プロパティを示すために呼び出されます。 エコー効果を実装するため、この例では 1 秒間のオーディオ データを格納するバッファーを使用します。 このメソッドは、オーディオをエンコードするサンプル レートに基づいて、バッファーのサイズを 1 秒間のオーディオのサンプル数に初期化する機会を提供します。 遅延効果では、遅延バッファー内の現在位置を追跡する整数カウンターも使います。 オーディオ パイプラインに効果が追加されるたびに 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 メソッドは、効果によって、ストリームのオーディオ データを変更するためのメソッドです。 このメソッドはフレームごとに 1 回呼び出され、ProcessAudioFrameContext オブジェクトが渡されます。 このオブジェクトには、処理対象の着信フレームを格納する入力 AudioFrame オブジェクトと、オーディオ パイプラインの残りの部分に渡すオーディオ データを書き込む出力 AudioFrame オブジェクトが含まれています。 オーディオ フレームは、オーディオ データの短いスライスを表す、オーディオ サンプル バッファーです。

AudioFrame のデータ バッファーにアクセスするには COM 相互運用機能が必要であるため、効果のクラス ファイルに 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 を取得します。 その後、実際のデータ バッファーを取得するために、先ほど定義した COM 相互運用機能のインターフェイス (IMemoryByteAccess) として IMemoryBufferReference オブジェクトをキャストし、GetBuffer を呼び出します。

これで、データ バッファーが取得され、入力バッファーからの読み取りと出力バッファーへの書き込みが可能になります。 入力バッファーの各サンプルで値が取得され、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 メソッド

システムは、効果をシャットダウンする必要があるときに、クラスで Close Close メソッドを呼び出します。 このメソッドを使って、作成したすべてのリソースを破棄する必要があります。 このメソッドの 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 プロパティ

効果がその出力を ProcessFrame に渡される ProcessAudioFrameContextOutputFrame ではなく、InputFrame のオーディオ バッファーに書き込む場合には、UseInputFrameForOutput プロパティを true に設定します。

public bool UseInputFrameForOutput { get { return false; } }

アプリへのカスタム効果の追加

アプリからオーディオ特殊効果を使うには、効果のプロジェクトへの参照をアプリに追加する必要があります。

  1. ソリューション エクスプローラーで、アプリのプロジェクトの下にある [参照設定] を右クリックし、[参照の追加] を選択します。
  2. [プロジェクト] タブを展開し、[ソリューション] を選択して、効果のプロジェクトの名前に対応するチェック ボックスをオンにします。 この例では、AudioEffectComponent という名前です。
  3. [OK]

オーディオ効果クラスが別の名前空間で宣言されている場合は、その名前空間をコード ファイルに含めるようにします。

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