버전

menu_open
Wwise SDK 2023.1.3
예제: 로우패스 필터 플러그인 개발하기

이 주제는 처음부터 끝까지 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:
//...
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)
{
// 채널별 로우패스 필터링 적용.
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.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)
{
// 여기서 기본 매개 변수를 초기화
RTPC.fFrequency = 1000.0f;
//...
}
//...
}
AKRESULT LowpassFXParams::SetParamsBlock(const void* in_pParamsBlock, AkUInt32 in_ulBlockSize)
{
//...
// 여기서 뱅크 데이터 읽기
RTPC.fFrequency = READBANKDATA(AkReal32, pParamsBlock, in_ulBlockSize);
//...
}
AKRESULT LowpassFXParams::SetParam(AkPluginParamID in_paramID, const void* in_pValue, AkUInt32 in_ulParamSize)
{
//...
// 여기서 매개 변수 변경 처리
case PARAM_FREQUENCY_ID:
RTPC.fFrequency = *((AkReal32*)in_pValue);
//...
}

세 번째로, WwisePlugin 폴더에서 Lowpass::GetBankParameters 함수를 업데이트해 뱅크의 'Frequency' 매개 변수를 작성해야 합니다.

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)

현재의 샘플 레이트를 구하고,

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

math 심볼을 포함시킨 다음,

// 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.cppExecute 함수 상단에서 빈도수 매개 변수가 변경됐는지 확인합니다. 이를 위해서는 간단히 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;
}
//...
}

이 데이터로 프레임의 각 샘플에 대해 coeffBegincoeffStep 만큼 증가시킵니다. 이 작업을 인/아웃 버퍼의 각 채널에 대해 실행해야 합니다.

void LowpassFX::Execute(AkAudioBuffer* io_pBuffer)
{
//...
for (AkUInt32 i = 0; i < uNumChannels; ++i)
{
coeffBegin = m_coeff; // 램프(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; // 다음 프레임을 위해 현재 값 저장.
}

차단 주파수를 실시간으로 제어하는 간단한 로우패스 필터를 구현하는 기본 작동 플러그인을 만들었으니 이제 설계 측면에 대해 살펴보겠습니다.

프로세스 캡슐화하기

이 시점에서 모든 신호 처리 논리는 플러그인 메인 클래스 안에 작성돼있습니다. 이는 다음과 같은 여러 가지 이유로 그다지 좋은 설계 패턴이 아닙니다.

  • 메인 플러그인 클래스의 부피를 키우고, 복잡한 효과를 빌드하는 처리를 새로 더함에 따라 상황이 더 안 좋아집니다.
  • 다른 플러그인에서 필터가 필요한 경우 해당 필터를 재사용하기 어려워지며, 특히 이런 종류의 기본 처리 단위를 사용하는 경우에는 더욱 그렇습니다.
  • 단일 책임 원칙에 부합하지 않습니다.

자체 클래스에서 필터 처리를 캡슐화하도록 코드를 리팩토링해보겠습니다. 다음 정의를 사용하여 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);
}
}
AkSampleType * GetChannel(AkUInt32 in_uIndex)
Definition: AkCommonDefs.h:575
uint16_t AkUInt16
Unsigned 16-bit integer
Audiokinetic namespace
Interface used to write data during sound bank generation.
AkForceInline AkUInt32 NumChannels() const
Get the number of channels.
Definition: AkCommonDefs.h:491
AKRESULT
Standard function call result.
Definition: AkTypes.h:213
AKSOUNDENGINE_API AKRESULT Init(const AkCommSettings &in_settings)
bool WriteReal32(float in_value)
Writes a 32-bit, single-precision floating point value.
float AkReal32
32-bit floating point
AkUInt16 uValidFrames
Number of valid sample frames in the audio buffer
Definition: AkCommonDefs.h:656
#define READBANKDATA(_Type, _Ptr, _Size)
Read and return bank data of a given type, incrementing running pointer and decrementing block size f...
uint32_t AkUInt32
Unsigned 32-bit integer
AkInt16 AkPluginParamID
Source or effect plug-in parameter ID
Definition: AkTypes.h:148
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:45

이 페이지가 도움이 되었나요?

지원이 필요하신가요?

질문이 있으신가요? 문제를 겪고 계신가요? 더 많은 정보가 필요하신가요? 저희에게 문의해주시면 도와드리겠습니다!

지원 페이지를 방문해 주세요

작업하는 프로젝트에 대해 알려주세요. 언제든지 도와드릴 준비가 되어 있습니다.

프로젝트를 등록하세요. 아무런 조건이나 의무 사항 없이 빠른 시작을 도와드리겠습니다.

Wwise를 시작해 보세요