版本

menu_open
Wwise SDK 2021.1.14
开发低通滤波器插件

创建新的工程

首先,需要使用 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:
//...
AkReal32 m_coeff;
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);
//...
}
void LowpassFX::Execute(AkAudioBuffer* io_pBuffer)
{
const AkUInt32 uNumChannels = io_pBuffer->NumChannels();
AkUInt16 uFramesProcessed;
for (AkUInt32 i = 0; i < uNumChannels; ++i)
{
uFramesProcessed = 0;
while (uFramesProcessed < io_pBuffer->uValidFrames)
{
// Apply lowpass filtering per channel.
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.hLowpassFXParams.cpp 来反映属性更改。

LowpassFXParams.h 中,更新 LowpassRTPCParams 结构中的参数 ID 和参数名称。

static const AkPluginParamID PARAM_FREQUENCY_ID = 0;
struct LowpassRTPCParams
{
AkReal32 fFrequency;
};

同时更新 LowpassFXParams.cpp

AKRESULT LowpassFXParams::Init(AK::IAkPluginMemAlloc* in_pAllocator, const void* in_pParamsBlock, AkUInt32 in_ulBlockSize)
{
if (in_ulBlockSize == 0)
{
// Initialize default parameters here
RTPC.fFrequency = 1000.0f;
//...
}
//...
}
AKRESULT LowpassFXParams::SetParamsBlock(const void* in_pParamsBlock, AkUInt32 in_ulBlockSize)
{
//...
// Read bank data here
RTPC.fFrequency = READBANKDATA(AkReal32, pParamsBlock, in_ulBlockSize);
//...
}
AKRESULT LowpassFXParams::SetParam(AkPluginParamID in_paramID, const void* in_pValue, AkUInt32 in_ulParamSize)
{
//...
// Handle parameter change here
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
{
// Write bank data here
in_pDataWriter->WriteReal32(m_propertySet.GetReal32(in_guidPlatform, "Frequency"));
return true;
}

最后,在处理循环中,还要使用以下公式来依据当前频率计算滤波器的系数:

coeff = exp(-2 * pi * f / sr)

我们需要检索当前采样率。

// LowpassFX.h
private:
//...
AkUInt32 m_sampleRate;
// LowpassFX.cpp
{
//...
m_sampleRate = in_rFormat.uSampleRate;
//...
}

加入一些数学符号。

// LowpassFX.cpp
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265359
#endif
//...

计算滤波器系数:

// LowpassFX.cpp
void LowpassFX::Execute(AkAudioBuffer* io_pBuffer)
{
m_coeff = static_cast<AkReal32>(exp(-2.0 * M_PI * m_pParams->RTPC.fFrequency / m_sampleRate));
//...
}

插入参数值

通常,每个缓冲区大小更新一次处理参数是不够的(缓冲区大小等于声道缓冲区中的采样数,一般介于 64 ~ 2048 之间)。尤其是在此参数会影响处理的频率或增益的情况下,数值更新太慢会导致输出声音中出现拉链噪声或噼啪噪声。

对此,只需在整个缓冲区当中以线性方式插入数值即可。下面我们来针对频率参数执行此操作。

在计算一帧新的音频样本之前(即在 LowpassFX.cpp 中的 Execute 函数最上面),我们要检查频率参数是否发生了更改。 为此,可直接查询 LowpassFXParams 类中的 AkFXParameterChangeHandler 对象。若频率发生了更改,则计算 ramp 的变量:

备注: AkAudioBuffer 对象的成员变量 uValidFrames 代表缓冲区中每个声道所含的有效采样数。
void LowpassFX::Execute(AkAudioBuffer* io_pBuffer)
{
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 即可。我们需要针对输入/输出缓冲区的每个声道执行此操作。

void LowpassFX::Execute(AkAudioBuffer* io_pBuffer)
{
//...
for (AkUInt32 i = 0; i < uNumChannels; ++i)
{
coeffBegin = m_coeff; // restart the ramp.
uFramesProcessed = 0;
while (uFramesProcessed < io_pBuffer->uValidFrames)
{
m_previousOutput[i] = pBuf[uFramesProcessed] =
pBuf[uFramesProcessed] + (m_previousOutput[i] - pBuf[uFramesProcessed]) * coeffBegin;
coeffBegin += coeffStep; // increase by coeffStep every sample.
++uFramesProcessed;
}
}
m_coeff = coeffBegin; // save the current value for the next frame.
}

现在我们构建了一个能够实现简单的低通滤波效果并实时控制截止频率的基础功能插件。接下来,我们说说设计方面的一些问题。

封装处理逻辑

现在,所有的信号处理逻辑都写在了插件主类内。这种设计模式存在很多弊端:

  • 插件主类会显得很臃肿,等到添加新的处理功能来构建较为复杂的效果时,情况会更加糟糕。
  • 很难在需要时将该滤波器用于其他插件。对这种基础处理组件来说更是如此。
  • 不符合单一职责原则。

下面我们来重构代码以将滤波处理封装到其自身的类中。首先,依据以下定义在 SoundEnginePlugin 文件夹中创建 FirstOrderLowpass.h 文件:

// FirstOrderLowpass.h
#pragma once
class FirstOrderLowpass
{
public:
FirstOrderLowpass();
~FirstOrderLowpass();
void Setup(AkUInt32 in_sampleRate);
void SetFrequency(AkReal32 in_newFrequency);
void Execute(AkReal32* io_pBuffer, AkUInt16 in_uValidFrames);
private:
AkUInt32 m_sampleRate;
AkReal32 m_frequency;
AkReal32 m_coeff;
AkReal32 m_previousOutput;
bool m_frequencyChanged;
};

接着,在 FirstOrderLowpass.cpp 文件中添加实现代码:

// 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;
}
}
void FirstOrderLowpass::Execute(AkReal32* io_pBuffer, AkUInt16 in_uValidFrames)
{
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;
}
AkUInt16 uFramesProcessed = 0;
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 函数来使用它们。

// LowpassFX.h
#include "FirstOrderLowpass.h"
#include <vector>
//...
private:
std::vector<FirstOrderLowpass> m_filter;
// LowpassFX.cpp
{
//...
m_filter.resize(in_rFormat.GetNumChannels());
for (auto & filterChannel : m_filter) { filterChannel.Setup(in_rFormat.uSampleRate); }
//...
}
void LowpassFX::Execute(AkAudioBuffer* io_pBuffer)
{
if (m_pParams->m_paramChangeHandler.HasChanged(PARAM_FREQUENCY_ID))
{
for (auto & filterChannel : m_filter) { filterChannel.SetFrequency(m_pParams->RTPC.fFrequency); }
}
const AkUInt32 uNumChannels = io_pBuffer->NumChannels();
for (AkUInt32 i = 0; i < uNumChannels; ++i)
{
m_filter[i].Execute(pBuf, io_pBuffer->uValidFrames);
}
}
float AkReal32
32-bit floating point
Definition: AkTypes.h:70
AkSampleType * GetChannel(AkUInt32 in_uIndex)
Definition: AkCommonDefs.h:549
Audiokinetic namespace
AkForceInline AkUInt32 NumChannels() const
Get the number of channels.
Definition: AkCommonDefs.h:465
AKRESULT
Standard function call result.
Definition: AkTypes.h:132
AKSOUNDENGINE_API AKRESULT Init(const AkCommSettings &in_settings)
AkUInt16 uValidFrames
Number of valid sample frames in the audio buffer
Definition: AkCommonDefs.h:630
#define READBANKDATA(_Type, _Ptr, _Size)
Read and return bank data of a given type, incrementing running pointer and decrementing block size f...
uint16_t AkUInt16
Unsigned 16-bit integer
Definition: AkTypes.h:58
uint32_t AkUInt32
Unsigned 32-bit integer
Definition: AkTypes.h:59
AkInt16 AkPluginParamID
Source or effect plug-in parameter ID
Definition: AkTypes.h:76
AkForceInline AkUInt32 GetNumChannels() const
Definition: AkCommonDefs.h:75
Defines the parameters of an audio buffer format.
Definition: AkCommonDefs.h:63
AkUInt32 uSampleRate
Number of samples per second
Definition: AkCommonDefs.h:64
#define AK_RESTRICT
Refers to the __restrict compilation flag available on some platforms
Definition: AkTypes.h:43

此页面对您是否有帮助?

需要技术支持?

仍有疑问?或者问题?需要更多信息?欢迎联系我们,我们可以提供帮助!

查看我们的“技术支持”页面

介绍一下自己的项目。我们会竭力为您提供帮助。

来注册自己的项目,我们帮您快速入门,不带任何附加条件!

开始 Wwise 之旅