이 주제는 처음부터 끝까지 Community 플러그인 개발의 예제를 사용합니다. 여기서는 로우패스 필터 플러그인을 만들어보겠습니다.
새 프로젝트 만들기
먼저 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의 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 매개 변수를 사용하도록 하려면 변경해야 할 것이 네 가지 있습니다.
먼저, WwisePlugin/Lowpass.xml
에 해당 정의를 추가해야 합니다. 플러그인 템플릿에 이미 'PlaceHolder'라는 기본 매개 변수가 있습니다. 이것을 사용해 'Frequency(빈도수)' 매개 변수를 정의합니다. WwisePlugin/Lowpass.xml
에서, 자리 표시자(placeholder) 속성을 다음으로 바꾸세요:
<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' 매개 변수를 작성해야 합니다.
{
in_pDataWriter->
WriteReal32(m_propertySet.GetReal32(in_guidPlatform,
"Frequency"));
return true;
}
마지막으로, 프로세스 순환 과정에서 현재 빈도수를 이용해 다음 공식으로 필터의 계수를 계산합니다.
coeff = exp(-2 * pi * f / sr)
현재의 샘플 레이트를 구하고,
math 심볼을 포함시킨 다음,
#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)의 변수를 계산합니다.
| 참고: 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;
}
차단 주파수를 실시간으로 제어하는 간단한 로우패스 필터를 구현하는 기본 작동 플러그인을 만들었으니 이제 설계 측면에 대해 살펴보겠습니다.
프로세스 캡슐화하기
이 시점에서 모든 신호 처리 논리는 플러그인 메인 클래스 안에 작성돼있습니다. 이는 다음과 같은 여러 가지 이유로 그다지 좋은 설계 패턴이 아닙니다.
- 메인 플러그인 클래스의 부피를 키우고, 복잡한 효과를 빌드하는 처리를 새로 더함에 따라 상황이 더 안 좋아집니다.
- 다른 플러그인에서 필터가 필요한 경우 해당 필터를 재사용하기 어려워지며, 특히 이런 종류의 기본 처리 단위를 사용하는 경우에는 더욱 그렇습니다.
- 단일 책임 원칙에 부합하지 않습니다.
자체 클래스에서 필터 처리를 캡슐화하도록 코드를 리팩토링해보겠습니다. 다음 정의를 사용하여 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)
{
}
}