创建新的工程
首先,需要使用 wp.py 工具创建新的插件工程。有关 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 上的设计工具平台。为此,我们来调用 premake
:
python "%WWISEROOT%/Scripts/Build/Plugins/wp.py" premake Authoring
现在创建好了用于构建声音引擎和设计工具 (WwisePlugin) 部分的解决方案。
接下来,便可构建自研插件并确认是否能在 Wwise 中加载。
python "%WWISEROOT%/Scripts/Build/Plugins/wp.py" build -c Release -x x64 -t vc160 Authoring
实现滤波处理效果
现在,我们想添加一些处理效果来进一步增强插件的功能。在此,我们使用以下公式来实现简单的一阶低通滤波效果:
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 参数,我们需要实施以下四项更改。
首先,必须在 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>
其次,需要更新 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);
}
再次,在 WwisePlugin
文件夹中,需要更新 Lowpass::GetBankParameters
函数,以将 Frequency 参数写入到 SoundBank 中:
bool LowpassPlugin::GetBankParameters(const GUID & in_guidPlatform, AK::Wwise::Plugin::DataWriter* in_pDataWriter) const
{
in_pDataWriter->WriteReal32(m_propertySet.GetReal32(in_guidPlatform, "Frequency"));
return true;
}
最后,在处理循环中,还要使用以下公式来依据当前频率计算滤波器的系数:
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));
}
插入参数值
通常,每个缓冲区大小更新一次处理参数是不够的(缓冲区大小等于声道缓冲区中的采样数,一般介于 64 ~ 2048 之间)。尤其是在此参数会影响处理的频率或增益的情况下,数值更新太慢会导致输出声音中出现拉链噪声或噼啪噪声。
对此,只需在整个缓冲区当中以线性方式插入数值即可。下面我们来针对频率参数执行此操作。
在计算一帧新的音频样本之前(即在 LowpassFX.cpp
中的 Execute
函数最上面),我们要检查频率参数是否发生了更改。 为此,可直接查询 LowpassFXParams
类中的 AkFXParameterChangeHandler
对象。若频率发生了更改,则计算 ramp 的变量:
{
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;
}
现在我们构建了一个能够实现简单的低通滤波效果并实时控制截止频率的基础功能插件。接下来,我们说说设计方面的一些问题。
封装处理逻辑
现在,所有的信号处理逻辑都写在了插件主类内。这种设计模式存在很多弊端:
- 插件主类会显得很臃肿,等到添加新的处理功能来构建较为复杂的效果时,情况会更加糟糕。
- 很难在需要时将该滤波器用于其他插件。对这种基础处理组件来说更是如此。
- 不符合单一职责原则。
下面我们来重构代码以将滤波处理封装到其自身的类中。首先,依据以下定义在 SoundEnginePlugin
文件夹中创建 FirstOrderLowpass.h
文件:
#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
对象(每个声道一个),然后通过调用 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)
{
}
}