This topic provides an example of Community plug-in development from start to finish. In this case, we will develop a lowpass filter plug-in.
新しいプロジェクトを作成する
まず最初に、 wp.py ツールで新しいプラグインプロジェクトを作成します。 Creating Audio Plug-ins に、 new
引数の詳しい内容があります。
python "%WWISEROOT%/Scripts/Build/Plugins/wp.py" new --effect -a author_name -n Lowpass -t "First Order Lowpass" -d "Simple First Order Lowpass Filter"
cd Lowpass
WindowsでAuthoringプラットフォームをターゲットとするので、 premake
をコールします:
python "%WWISEROOT%/Scripts/Build/Plugins/wp.py" premake Authoring
SoundEngine部分とAuthoring (WwisePlugin) 部分をビルドするためのソリューションが作成されています。
これで、プラグインをビルドしてから、Wwiseでロードされることを確認します。
python "%WWISEROOT%/Scripts/Build/Plugins/wp.py" build -c Release -x x64 -t vc160 Authoring
フィルタプロセスを実装する
次に、このプラグインがもう少し役に立つように、処理能力を少し追加します。この式を使って簡単な1次ローパスフィルタを実装します:
y[n] = x[n] + (y[n-1] - x[n]) * coeff
このとき coeff
は0から1の間の浮動小数点とします。
最初に、このフィルタのデータを保持できるように、 SoundEnginePlugin/LowpassFX.h
にいくつかの変数を作成します。
#include <vector>
private:
std::vector<AkReal32> m_previousOutput;
変数値 m_coeff
は浮動小数点としてのフィルタ係数で、全サウンドチャンネルに使います。ベクトル m_previousOutput
が全チャンネルの最後のアウトプット値を保持しますが、これはフィルタの次の値を計算するために必須です。
フィルタエフェクトを適用するには、係数変数を初期化し、チャンネル数に従いベクトルサイズを調整し、各サンプルを前の公式で処理するだけです。
SoundEnginePlugin/LowpassFX.cpp
で:
LowpassFX::LowpassFX()
:
, m_coeff(0.99f)
AKRESULT LowpassFX::
Init(
AK::IAkPluginMemAlloc* in_pAllocator,
AK::IAkEffectPluginContext* in_pContext,
AK::IAkPluginParam* in_pParams,
AkAudioFormat& in_rFormat)
{
m_previousOutput.resize(in_rFormat.GetNumChannels(), 0.0f);
}
{
for (
AkUInt32 i = 0; i < uNumChannels; ++i)
{
uFramesProcessed = 0;
while (uFramesProcessed < io_pBuffer->uValidFrames)
{
m_previousOutput[i] = pBuf[uFramesProcessed] =
pBuf[uFramesProcessed] + (m_previousOutput[i] - pBuf[uFramesProcessed]) * m_coeff;
++uFramesProcessed;
}
}
}
フィルタの周波数を制御するために、RTPCパラメータを使う
今は、このフィルタと何のやり取りもできないので、面白みのないフィルタです。それでは、フィルタの周波数にRTPCパラメータをバインドし、リアルタイムでその値を変更できるようにします。プラグインがRTPCパラメータを使えるように、4つの変更を行います。
最初に、その定義を WwisePlugin/Lowpass.xml
に追加します。プラグインテンプレートに、既に"PlaceHolder"という名のパラメータスケルトンがあります。 これを使って"Frequency"パラメータを定義します。 WwisePlugin/Lowpass.xml
で、プレースホルダとしてのプロパティを、以下に置き換えます:
<Property Name="Frequency" Type="Real32" SupportRTPCType="Exclusive" DisplayName="Cutoff Frequency">
<UserInterface Step="0.1" Fine="0.001" Decimals="3" UIMax="10000" />
<DefaultValue>1000.0</DefaultValue>
<AudioEnginePropertyID>0</AudioEnginePropertyID>
<Restrictions>
<ValueRestriction>
<Range Type="Real32">
<Min>20.0</Min>
<Max>10000.0</Max>
</Range>
</ValueRestriction>
</Restrictions>
</Property>
2つ目に、SoundEnginePluginフォルダの中で、 LowpassFXParams.h
と LowpassFXParams.cpp
を更新し、プロパティの変更を反映させます。
LowpassFXParams.h
で、 LowpassRTPCParams
ストラクチャの中のパラメータIDとパラメータ名を更新します。
struct LowpassRTPCParams
{
};
LowpassFXParams.cpp
も更新します:
{
if (in_ulBlockSize == 0)
{
RTPC.fFrequency = 1000.0f;
}
}
AKRESULT LowpassFXParams::SetParamsBlock(
const void* in_pParamsBlock,
AkUInt32 in_ulBlockSize)
{
}
{
case PARAM_FREQUENCY_ID:
RTPC.fFrequency = *((
AkReal32*)in_pValue);
}
3つ目に、 WwisePlugin
フォルダで、バンクに"Frequency"パラメータを書くために Lowpass::GetBankParameters
関数を更新します。
{
in_pDataWriter->
WriteReal32(m_propertySet.GetReal32(in_guidPlatform,
"Frequency"));
return true;
}
最後に、処理ループでこの公式(formula)を使い、現在の周波数を使ってフィルタ係数を計算します。
coeff = exp(-2 * pi * f / sr)
現在のサンプリングレートを取得する必要があります。
数学記号を含め、
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265359
#endif
フィルタ係数を計算します:
{
m_coeff =
static_cast<AkReal32>(exp(-2.0 * M_PI * m_pParams->RTPC.fFrequency / m_sampleRate));
}
パラメータ値を補間する
処理パラメータをバッファサイズ(1つのオーディオバッファチャンネルにおけるサンプル数のことで、一般的に64から2048の間)ごとに一回だけ更新するのでは、不十分であることが多いです。特に、処理周波数やゲインにこのパラメータが影響するのであれば、値をあまりゆっくり更新したのでは、アウトプットサウンドでジッパーノイズやクリックが発生してしまいます。
この問題のシンプルな対策として、バッファ全体に渡り、値を線形補間することができます。今回の周波数パラメータではどのように行うのかを、以下に示します。
オーディオサンプルの新しいフレームを計算する直前、つまり LowpassFX.cpp
の、 Execute
関数の一番上で、周波数パラメータが変化したかどうかをチェックします。 チェックするには、 LowpassFXParams
クラスの AkFXParameterChangeHandler
オブジェクトに聞けばいいのです。周波数に変化があれば、ランプ(ramp)の変数を計算します:
info | 注釈: AkAudioBuffer オブジェクトのメンバ変数 uValidFrames は、バッファに含まれるチャンネルごとの有効サンプル数を表します。 |
{
AkReal32 coeffBegin = m_coeff, coeffEnd = 0.0f, coeffStep = 0.0f;
if (m_pParams->m_paramChangeHandler.HasChanged(PARAM_FREQUENCY_ID))
{
coeffEnd =
static_cast<AkReal32>(exp(-2.0 * M_PI * m_pParams->RTPC.fFrequency / m_sampleRate));
coeffStep = (coeffEnd - coeffBegin) / io_pBuffer->
uValidFrames;
}
}
このデータがあれば、フレームの各サンプルで、 coeffBegin
を coeffStep
だけ増やせばいいのです。これを、イン・アウトバッファの、各チャンネルに対して行う必要があります。
{
for (
AkUInt32 i = 0; i < uNumChannels; ++i)
{
coeffBegin = m_coeff;
uFramesProcessed = 0;
while (uFramesProcessed < io_pBuffer->uValidFrames)
{
m_previousOutput[i] = pBuf[uFramesProcessed] =
pBuf[uFramesProcessed] + (m_previousOutput[i] - pBuf[uFramesProcessed]) * coeffBegin;
coeffBegin += coeffStep;
++uFramesProcessed;
}
}
m_coeff = coeffBegin;
}
さて、カットオフ周波数をリアルタイムで制御できる単純なローパスフィルタを実装した、基本的で機能的なプラグインができたところで、デザイン上の懸念点について考えます。
処理のカプセル化
今、全ての信号処理ロジックがプラグインのメインクラスに記述されています。これは悪いデザインパターンで、その理由は以下のように多数あります:
- メインプラグインクラスが膨張していて、今後、複雑なエフェクトをビルドするために新しい処理過程を追加していった場合は、さらにひどくなるはずです。
- このフィルタをほかのプラグインで使いたくても、再利用しにくいのですが、このような基本的な処理ユニットは、絶対に再利用したくなります!
- 単一責任の原則(single responsibility principle)に準拠していません。
そこで、コードのリファクタリングを行い、フィルタ処理を、自分自身のクラスにカプセル化します。ファイル FirstOrderLowpass.h
を SoundEnginePlugin
フォルダに作成し、このように定義します:
#pragma once
class FirstOrderLowpass
{
public:
FirstOrderLowpass();
~FirstOrderLowpass();
void SetFrequency(
AkReal32 in_newFrequency);
private:
bool m_frequencyChanged;
};
そして、この実装を FirstOrderLowpass.cpp
と呼ばれるファイルに追加します:
#include "FirstOrderLowpass.h"
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265359
#endif
FirstOrderLowpass::FirstOrderLowpass()
: m_sampleRate(0)
, m_frequency(0.0f)
, m_coeff(0.0f)
, m_previousOutput(0.0f)
, m_frequencyChanged(false)
{
}
FirstOrderLowpass::~FirstOrderLowpass() {}
void FirstOrderLowpass::Setup(
AkUInt32 in_sampleRate)
{
m_sampleRate = in_sampleRate;
}
void FirstOrderLowpass::SetFrequency(
AkReal32 in_newFrequency)
{
if (m_sampleRate > 0)
{
m_frequency = in_newFrequency;
m_frequencyChanged = true;
}
}
{
AkReal32 coeffBegin = m_coeff, coeffEnd = 0.0f, coeffStep = 0.0f;
if (m_frequencyChanged)
{
coeffEnd =
static_cast<AkReal32>(exp(-2.0 * M_PI * m_frequency / m_sampleRate));
coeffStep = (coeffEnd - coeffBegin) / in_uValidFrames;
m_frequencyChanged = false;
}
while (uFramesProcessed < in_uValidFrames)
{
m_previousOutput = io_pBuffer[uFramesProcessed] =
io_pBuffer[uFramesProcessed] + (m_previousOutput - io_pBuffer[uFramesProcessed]) * coeffBegin;
coeffBegin += coeffStep;
++uFramesProcessed;
}
m_coeff = coeffBegin;
}
これで、メインプラグインクラスでは、 FirstOrderLowpass
オブジェクト(オーディオチャンネル1つに対し、1つ)のベクトルを作成し、その Setup
関数をコールし、それらを使い始めるだけでいいのです。
#include "FirstOrderLowpass.h"
#include <vector>
private:
std::vector<FirstOrderLowpass> m_filter;
{
for (
auto & filterChannel : m_filter) { filterChannel.Setup(in_rFormat.
uSampleRate); }
}
{
if (m_pParams->m_paramChangeHandler.HasChanged(PARAM_FREQUENCY_ID))
{
for (auto & filterChannel : m_filter) { filterChannel.SetFrequency(m_pParams->RTPC.fFrequency); }
}
for (
AkUInt32 i = 0; i < uNumChannels; ++i)
{
}
}