目录

Wwise SDK 2018.1.11
如何创建 Wwise 声音引擎插件

Wwise 声音引擎插件的创建可以很有创意,但也可能会很复杂。我们强烈建议您仔细阅读以下章节,并结合查看 示例插件 部分提供的示例。您可以参考示例中提供的标准样式和结构来构建插件。

Wwise 声音引擎插件概述

插件可以让您将自定义 DSP 程序插入由声音引擎执行的整体信号处理链中。插件参数既可在设计工具中控制,也可在游戏中通过 RTPC 控制(请参阅 概念:实时参数控制(RTPC) )。

每个插件包括 2 个组件:

  • 运行时组件,它集成在声音引擎中,可针对跨平台开发,或针对特定平台优化。游戏需要注册此插件(请参阅 插件静态注册 )并与其实现做链接。
  • 集成在设计工具中的用户界面组件。此组件位于由 Wwise 加载的 DLL 中(请参阅 如何创建 Wwise 插件 DLL )。UI 中确定的所有插件参数存储在 SoundBank 中,因此游戏中不使用此组件。如果参数将通过 RTPC 在游戏中进行控制,则必要的信息也会存储在 SoundBank 中。
Tip:为运行时组件创建通用静态库。通过这种方法,游戏和插件用户界面(Wwise 加载的 DLL)都可以链接它。

需要实现参数节点接口,以对来自声音引擎 RTPC 管理器或 Wwise 设计工具的更改做出反应,并在执行期间获取当前插件参数。(请参阅 参数节点接口的实现 。)

可集成到声音引擎中的插件主要有 3 类:

插件及其相关参数接口由声音引擎通过插件机制创建。该插件机制要求暴露静态创建函数,这些函数在必要时返回新参数节点和新插件实例。以下代码演示如何可以做到这一点。创建函数必须打包到插件库用户可见的 AK::PluginRegistration 静态实例中。每个插件类别/类型需要一个 AK::PluginRegistration 类。

// 静态参数节点创建函数回调将注册到插件管理器中。
AK::IAkPluginParam * CreateMyPluginParams( AK::IAkPluginMemAlloc * in_pAllocator )
{
return AK_PLUGIN_NEW( in_pAllocator, CAkMyPluginParams() );
}
// 静态插件创建函数回调将注册到插件管理器中。
AK::IAkPlugin * CreateMyPlugin( AK::IAkPluginMemAlloc * in_pAllocator )
{
return AK_PLUGIN_NEW( in_pAllocator, CAkMyPlugin() );
}
//静态初始化器对象将自动把插件注册到声音引擎中。
AK::PluginRegistration MyPluginRegistration(AkPluginTypeEffect, AKCOMPANYID_MYCOMPANY, EFFECTID_MYPLUGIN, CreateMyPlugin, CreateMyPluginParams);
Note: 根据您要制作的插件类型,设置 AK::PluginRegistration 函数的 AkPluginType 参数。例如,源插件的 AkPluginTypeSource、效果器插件的 AkPluginTypeEffect 。请参阅 AkPluginType
Note: Registration 对象的命名非常重要。AK_STATIC_LINK_PLUGIN(pluginname) 宏将使用它,在 _pluginname_后加上“Registration”。请参见下文的 插件静态注册

如果您是插件提供者,希望确保您的插件不会与来自其它供应商的插件有相同的 ID,则请联系 support@audiokinetic.com 以获得预留的公司 ID。

插件静态注册

各种音频插件全部由 Plug-in Manager 处理,后者可根据 CompanyID 和 PluginID 识别不同的插件类别。插件必须先在 Plug-in Manager 中注册,然后才可在游戏中使用。注册过程将 PluginID 绑定到以参数形式提供的创建函数回调上。

以下例程演示游戏注册插件的方式。如果游戏要使用 Audiokinetic 提供的插件,则也必须先注册插件。

为了方便使用,所有插件还有一个出厂头文件,其中只包含 AK_STATIC_LINK_PLUGIN 宏。为了使插件管理更加方便,按照库名加上“Factory”的方式命名您的出厂头文件。例如,以下代码是与 MyPlugin.lib 相关联的 MyPluginFactory.h 的内容。

AK_STATIC_LINK_PLUGIN(MyPlugin); //此宏在声音引擎中注册插件。

使用 MyPlugin 的游戏仅包含 MyPluginFactory 文件,并与 MyPlugin.lib 链接。

#include <AK/Plugin/MyPluginFactory.h> // 出厂头文件强制通过 AK_STATIC_LINK_PLUGIN 链接插件代码。
Note: 如果您收到符号结尾定义了多重 _linkonceonly 的链接错误,这意味着您在多个 .cpp 文件中加入了 Factory include。每个链接单元(如 DLL、so、dylib、exe 等)中只需加入一次。

动态库

在游戏中使用插件的方式分为两种:一是通过静态库,二是通过动态库。静态库是必须要发布的。动态库则不强制发布,但强烈建议发布动态库,因为Unity中集成Wwise使用的是动态库。但是Wwise Unreal集成需要使用静态库。

使用静态库来创建动态库极其容易。Audiokinetic 对所有效果器插件都通过静态库创建了动态库,因此您可以在文件夹 \SDK\samples\DynamicLibraries 中找到大量的示例。您必须:

  • 保证工程与您构建的静态库链接。
  • 直接或者通过头文件加入 AK_STATIC_LINK_PLUGIN 的引用。
  • 确保您的动态库导出符号“g_pAKPluginList”。大多数平台将自动导出此符号,因为它被显性地声明为可导出符号。有时需要显性 DEF 文件。使用宏 DEFINE_PLUGIN_REGISTER_HOOK 定义符号。
  • 根据您的 XML 声明对动态库命名。如果您未在 XML 中指定 EngineDllName 属性,则以 XML 的名称对它命名。 注意,您可以把多个插件组合到一个 DLL 中。

简单来说,只需使用以下代码便可将静态库转换为动态库:

要在 Unity 中部署您的插件,请在 Wwise\Deployment\Plugins\[Platform]\DSP 文件夹中放置一份动态库 。位于该文件夹中的所有 DSP 插件应采用优化过的构建配置,这样才可以供发布版的游戏使用。

Note:iOS 中,构建系统阻止使用动态库 。因此在 Unity 中,您必须部署 .a 文件和相应的 Factory.h 文件。其它平台上使用的动态库与在 iOS 中使用的静态库之间的链接通过名称来完成:确保 DLL 名称与库名称(或子字符串)相同,不带“lib”前缀。 例如:

  • 在 Windows 中:MyPlugin.dll
  • 在 iOS 中:libMyPlugin.a + MyPluginFactory.h

Note:Mac 中,Wwise 将只加载 DYLIB 文件。然而,Unity 不将 DYLIB 视为有效的扩展名。因此在创建游戏时,它不会复制和部署这些文件。解决这一问题的方法是,把扩展名重命名为 BUNDLE,即使文件本身不是 BUNDLE。 例如:

  • 在 Windows 中:MyPlugin.dll
  • 在 Mac(非 Unity 游戏):MyPlugin.dylib
  • 在 Mac(Unity 游戏):MyPlugin.bundle

Note:Android中,您需要给动态库加“lib”前缀,比如libMyPlugin.so。

在音频插件中分配/取消分配内存

您应通过提供的内存分配器接口来执行音频插件中的所有动态内存分配或取消分配。这确保插件占用和释放的所有内存能够被 Memory Manager 跟踪,从特定内存池中分配,以及在 Wwise 性能分析器中显示。

提供了用于重载 new / delete 操作和使用随附的内存分配器调用 malloc() / free() 的宏。这些宏由 IAkPluginMemAlloc.h 提供。

要分配对象,则需使用 AK_PLUGIN_NEW() 宏,并将指针传递到内存分配器接口和期望的对象类型。宏返回指向新分配对象的指针。需要使用相应的 AK_PLUGIN_DELETE() 宏来释放内存。

要分配数组,则要使用 AK_PLUGIN_ALLOC(),后者从 Memory Manager 获取请求的大小(单位:比特)并将空指针返回给分配的内存地址。使用相应的 AK_PLUGIN_FREE() 宏释放分配的内存。以下例程演示如何使用这些宏。

// 分配一个 CAkMyObject
CAkMykObject * pSingleObject = AK_PLUGIN_NEW( in_pAllocator, CAkMyObject );
// 释放 pSingleObject
AK_PLUGIN_DELETE( in_pAllocator, pSingleObject );
// 分配一系列 uNumSamples 音频样本
AkSampleType * pData = static_cast<AkSampleType *>( AK_PLUGIN_ALLOC( in_pAllocator, sizeof(AkSampleType) * uNumSamples) );
// 释放一系列音频样本
AK_PLUGIN_FREE( in_pAllocator, pData );

参数节点接口的实现

参数节点在本质上集中了参数的读写访问权限。插件参数接口包括以下方法:

AK::IAkPluginParam::Clone()

此方法应该会创建一份完全相同的参数,并调整必要的内部状态变量,以便随时供新插件使用。例如,当事件创建新播放实例时,就会发生这种情况。此函数必须使用 AK_PLUGIN_NEW() 宏返回新的参数节点。在许多情况下,调用拷贝构造函数就够了(如下面的例程中所示)。如果在参数节点中分配内存,则应执行深度复制。

// 创建相同的共享参数。
AK::IAkPluginParam * CAkMyPluginParams::Clone( AK::IAkPluginMemAlloc * in_pAllocator )
{
return AK_PLUGIN_NEW( in_pAllocator, CAkMyPluginParams(*this) );
}

AK::IAkPluginParam::Init()

此函数使用提供的参数块对参数进行初始化。当提供的参数块大小为零(即当设计工具中使用此插件时),AK::IAkPluginParam::Init() 应使用默认值对参数结构进行初始化。

Tip:当参数块有效时,调用 AK::IAkPluginParam::SetParamsBlock() 对参数块进行初始化。
// 参数节点初始化。
AKRESULT CAkMyPluginParams::Init( AK::IAkPluginMemAlloc * in_pAllocator, void * in_pParamsBlock, AkUInt32 in_ulBlockSize )
{
if ( in_ulBlockSize == 0)
{
// 如果我们收到无效参数块,则使用默认值进行初始化。
return AK_Success;
}
return SetParamsBlock( in_pParamsBlock, in_ulBlockSize );
}

AK::IAkPluginParam::SetParamsBlock()

此方法使用在 Wwise 中创建 SoundBank 期间通过 AK::Wwise::IAudioPlugin::GetBankParameters() 存储的参数块一次性地设置全部插件参数。插件的 Wwise 设计工具版本将按照写入 SoundBank 的同一格式读取参数。注意,数据为压缩格式,因此根据某些目标平台所需的数据类型,可能无法对齐变量。使用 AkBankReadHelpers.h 中提供的 READBANKDATA 助手宏来避免这些与特定平台相关的问题。无需担心插件参数的字节顺序问题,因为应用程序已经正确地交换数据的字节。

// 读取并解析参数块。
AKRESULT CAkMyPluginParams::SetParamsBlock( void * in_pParamsBlock, AkUInt32 in_uBlockSize )
{
// 按照数据在 SoundBank 中的放置顺序读取数据
AKRESULT eResult = AK_Success;
AkUInt8 * pParamsBlock = (AkUInt8 *)in_pParamsBlock;
m_fFloatParam1 = READBANKDATA( AkReal32, pParamsBlock, in_ulBlockSize );
m_bBoolParam2 = READBANKDATA( bool, pParamsBlock, in_ulBlockSize );
CHECKBANKDATASIZE( in_ulBlockSize, eResult );
return eResult;
}

AK::IAkPluginParam::SetParam()

此方法一次更新一个参数。每当参数值发生变化时都会调用此方法,有的从插件 UI 调用,有的从 RTPC 调用,等等。要更新的参数由类型为 AkPluginParamID 的参数指定,对应于 Wwise XML 插件描述文件中定义的 AudioEnginePropertyID。(请参阅 Wwise 插件 XML 描述文件 了解更多信息)。

Tip:我们建议将 XML 文件中定义的每个 AudioEngineParameterID 绑定到 AkPluginParamsID 类型的常变量。
Note: 无论 XML 文件中指定的属性类型,支持 RTPC 的参数被赋于 AkReal32 类型。
// Wwise 的参数 ID 或 RTPC。
static const AkPluginParamID AK_MYFLOATPARAM1_ID = 0;
static const AkPluginParamID AK_MYBOOLPARAM2_ID = 1;
AKRESULT CAkMyPluginParams::SetParam( AkPluginParamID in_ParamID, void * in_pValue, AkUInt32 in_uParamSize )
{
// 设置参数值。
switch ( in_ParamID )
{
case AK_MYFLOATPARAM1_ID:
m_fFloatParam1.fParam2 = *(AkReal32*)( in_pValue );
break;
case AK_MYBOOLPARAM2_ID:
m_bBoolParam2 = *(bool*)( in_pValue ); // 参数不支持 RTPC
or ...
// 注意,无论 XML 插件描述中的属性类型,RTPC 参数始终属于浮点类型
m_bBoolParam2 = (*(AkReal32*)(in_pValue)) != 0;
break;
default:
}
return AK_Success;
}

AK::IAkPluginParam::Term()

当终止参数节点时,声音引擎将调用此方法。必须释放使用的任何内存资源,参数节点负责自行析构。

// 终止共享参数。
{
AK_PLUGIN_DELETE( in_pAllocator, this );
return AK_Success;
}

参数节点与插件之间的通信。

每个插件都有一个相关联的参数节点,插件可以从中获取参数值,相应的更新其 DSP。相关联的参数节点接口在插件初始化时传入,在插件的生命期间将一直保持有效。然后插件可以根据 DSP 处理的需要频繁地从参数节点中查询信息。由于插件与其相关联的参数节点之间为单向关系,因此要响应参数节点对参数值的查询,则要由实现方自主处理(例如使用访问器方法)。

音频插件接口的实现

要开发音频插件,必须实现特定函数,以让插件能够在引擎的音频数据流中正常工作。对于源插件,您应该继承 AK::IAkSourcePlugin 接口,对于可将输入缓冲区替换成输出的效果器(例如无需无序访问或改变数据速率),您应该继承 AK::IAkInPlaceEffectPlugin。对于需要实现非就地方法的其它效果器,您应继承 AK::IAkOutOfPlaceEffectPlugin 接口。

插件生命周期始终从调用 AK::IAkPlugin::Init() 函数开始,紧接着调用 AK::IAkPlugin::Reset()。只要插件需要输出更多数据,就会使用新的缓冲区调用 AK::IAkPlugin::Execute()。当不再需要插件时调用 AK::IAkPlugin::Term()

AK::IAkPlugin::Term()

在终止插件时调用此方法。AK::IAkPlugin::Term() 必须释放插件所使用的所有内存资源,并自行析构插件。

{
if ( m_pMyDelayLine != NULL )
{
AK_PLUGIN_FREE( in_pAllocator, m_pMyDelayLine );
m_pMyDelayLine = NULL;
}
AK_PLUGIN_DELETE( in_pAllocator, this );
return AK_Success;
}

AK::IAkPlugin::Reset()

复位方法必须重新初始化插件的状态,使它准备接纳新的无关音频内容。声音引擎管线将在初始化刚结束后和任何对象状态需要复位的时刻立即调用 AK::IAkPlugin::Reset()。一般所有内存分配都要在初始化时执行,但是在调用 AK::IAkPlugin::Reset() 时应清除例如延迟线状态和采样点计数。

// 复位插件的内部状态
{
// 复位延迟线
if ( m_pMyDelayLine != NULL )
memset( m_pMyDelayLine, 0, m_uNumDelaySamples * sizeof(AkSampleType) );
return AK_Success;
}

AK::IAkPlugin::GetPluginInfo()

当声音引擎需要有关插件的信息时,要使用此插件信息查询机制。在 AkPluginInfo 结构中填写正确的信息,以描述所实现的插件类型(例如源插件或效果器插件)、它的缓冲区使用方案(例如就地使用)和处理模式(例如同步)。

Note: 在所有其它平台上效果器插件应该是同步的。

// 声音引擎查询效果信息。
AKRESULT CAkMyPlugin::GetPluginInfo( AkPluginInfo & out_rPluginInfo )
{
out_rPluginInfo.eType = AkPluginTypeSource; // 源插件。
out_rPluginInfo.bIsInPlace = true; // In-place 插件。
out_rPluginInfo.bIsAsynchronous = false; // 同步插件。
return AK_Success;
}

访问使用 AkAudioBuffer 结构的数据

在执行时音频数据缓冲区通过指向 AkAudioBuffer 结构的指针传递给插件。传递给插件的所有音频缓冲区使用固定格式。对于支持软件效果器的平台,音频缓冲区的声道不是交错存取的,所有样本在 (-1.f,1.f) 范围为归一化 32 位浮点,以 48 kHz 采样率运行。

AkAudioBuffer 结构提供可访问交错存取和非交错存取数据的手段。它包含一个字段用来指定每个声道缓冲区中的有效采样帧数(AkAudioBuffer::uValidFrames)以及这些缓冲区可包含的最大采样帧数(由 AkAudioBuffer::MaxFrames() 返回)。

AkAudioBuffer 结构还包含缓冲区的声道掩码,此掩码定义数据中存在的声道。如果您仅需要声道数,则使用 AkAudioBuffer::NumChannels()

获取交错存取数据。

插件可通过 AkAudioBuffer::GetInterleavedData() 访问交错存取数据的缓冲区。只有源插件可访问和输出交错存取数据。为了做到这一点,它们必须在初始化期间正确准备声音引擎(请参阅 AK::IAkSourcePlugin::Init() )。声音引擎将处理的 DSP 实例化,以正确地把数据转换成内建管线格式。

Tip: 如果源插件输出数据本来符合声音引擎的内建格式,您则可以获得更高的性能。

获取非交错存取数据。

插件通过 AkAudioBuffer::GetChannel() 访问单个非交错存取声道。数据类型总是符合声音引擎的内建格式(AkSampleType)。以下例程显示如何获取所有非交错存取声道进行处理。

// 各自处理所有声道。
void CAkMyPlugin::Execute( AkAudioBuffer * io_pBuffer ) // 输入/输出缓冲区(现场执行处理)。
{
AkUInt32 uNumChannels = io_pBuffer->NumChannels();
for ( AkUInt32 uChan=0; uChan<uNumChannels; uChan++ )
{
AkSampleType * pChannel = io_pBuffer->GetChannel( uChan );
// 处理 pChannel 的数据……
}
}
Caution: 插件决不可想当然地认为每个声道的缓冲区在内存中是连续的。

声道顺序

声道排序如下:Front Left(左前)、Front Right(右前)、Center(中置)、Rear Left(左后)、Rear Right(右后),和 LFE。低频声道(LFE)总是布置在末尾,这是因为很多 DSP 运算需要单独来处理 LFE。插件可以使用 AkAudioBuffer::HasLFE() 来查询音频缓冲区中是否存在 LFE 声道。它可以通过调用 AkAudioBuffer::GetLFE() 直接访问 LFE 声道。以下代码演示独立处理 LFE 声道的两种不同方式。

void CAkMyPlugin::Execute( AkAudioBuffer * io_pBuffer ) // 输入/输出缓冲区(现场执行处理)。
{
// 使用 GetLFE() 获取 LFE 声道。
AkSampleType * pLFE = io_pBuffer->GetLFE();
if ( pLFE )
{
// 处理 pLFE 的数据……
}
OR ...
// 通过 GetChannel() 获取 LFE。
if ( io_pBuffer->HasLFE() )
{
AkSampleType * pLFE = io_pBuffer->GetChannel( io_pBuffer->NumChannels() - 1 );
// 处理 pLFE 的数据……
}
}

如果您要对非 LFE 的声道做特定处理,则需要使用 AkCommonDefs.h 的声道索引定义。 例如,如果您想只处理 5.x 配置的中置声道,则要执行以下操作:

// 只处理 5.x 配置的中置声道。
void CAkMyPlugin::Execute( AkAudioBuffer * io_pBuffer ) // 输入/输出缓冲区(现场执行处理)。
{
// 查询特定声道配置。
if ( io_pBuffer->GetChannelMask() == AK_SPEAKER_SETUP_5 || io_pBuffer->GetChannelMask() == AK_SPEAKER_SETUP_5POINT1 )
{
// 使用为相应配置定义的索引访问声道。
AkSampleType * pCenter = io_pBuffer->GetChannel( AK_IDX_SETUP_5_CENTER );
// 处理 pCenter 的数据……
}
}
Tip: 注意,声道索引定义与配置是 N.0 还是 N.1 无关。这是因为, LFE 声道始终是最后一个声道,除了源插件(如果声道配置没有LFE,则不应使用AK_IDX_SETUP_N_LFE)。
Note: 在7.1的情况下,源插件交错数据的声道顺序是 L-R-C-LFE-BL-BR-SL-SR。

在插件中使用全局声音引擎回调

插件可使用 AK::IAkGlobalPluginContext::RegisterGlobalCallback() 注册到各个全局声音引擎回调。例如,对于每个音频帧,可能需要通过插件知道的 Singleton(单例)对象调用一次插件类。您还可使用全局挂钩来初始化在声音引擎整个生命期间一直保留的数据结构。

为此,将默认的 AK_IMPLEMENT_PLUGIN_FACTORY 宏替换为您自己的实现,以利用 AK::PluginRegistration 的“RegisterCallback”。下面的代码片段中为此定义了一个静态回调 MyRegisterCallback,并将它传递给 AK::PluginRegistration 对象 MyPluginRegistration。

Note: AK_IMPLEMENT_PLUGIN_FACTORY 宏声明出厂函数和 AK::PluginRegistration 对象,将字符串附加到插件名称后面,名称是作为参数传递给它的。在下例中,AK_IMPLEMENT_PLUGIN_FACTORY 已按照相同的命名规则重新实现了。对于您自己的插件,应将“MyPlugin”替换成插件的实际名称。另外,AK_IMPLEMENT_PLUGIN_FACTORY 的姊妹宏 AK_STATIC_LINK_PLUGIN 遵循相同的命名规则。如果您没采用 AK_IMPLEMENT_PLUGIN_FACTORY 的命名规则,则还需要相应地重新实现 AK_STATIC_LINK_PLUGIN
// 用于初始化和终止 MyPluginManager (AkGlobalCallbackFunc) 的全局回调。
static void MyRegisterCallback(
AK::IAkGlobalPluginContext * in_pContext, ///< 引擎上下文。
AkGlobalCallbackLocation in_eLocation, ///< 启动此回调的位置。
void * in_pCookie ///< 用户 cookie 传递到 AK::SoundEngine::RegisterGlobalCallback()。
)
{
if (in_eLocation == AkGlobalCallbackLocation_Register)
{
// 注册时间:在声音引擎初始化或动态插件库加载时调用。
// 创建我们的单例模式。对于应该跨越声音引擎整个生命周期的分配,要使用全局上下文提供的分配器。
MyPluginManager * pMyPluginManager = AK_PLUGIN_NEW(in_pContext->GetAllocator(), MyPluginManager);
if (pMyPluginManager)
{
// 已成功。注册到“Term”回调来终止我们的管理器。
AKRESULT eResult = in_pContext->RegisterGlobalCallback(MyRegisterCallback, AkGlobalCallbackLocation_Term, pMyPluginManager);
// 处理注册故障。
if (eResult != AK_Success)
{
// ...
}
}
}
else if (in_eLocation == AkGlobalCallbackLocation_Term)
{
// 终止时间:在声音引擎终止时调用。
// cookie 是在上面注册的 MyPluginManager 的实例。
AKASSERT(in_pCookie);
// 销毁它。
AK_PLUGIN_DELETE(in_pContext->GetAllocator(), (MyPluginManager*)in_pCookie);
}
}
// 替换 AK_IMPLEMENT_PLUGIN_FACTORY(MyPlugin, AkPluginTypeEffect, MY_COMPANY_ID, MY_PLUGIN_ID)
// 在此,“MyPlugin”应替换成您插件的名称。
AK::IAkPlugin* CreateMyPlugin(AK::IAkPluginMemAlloc * in_pAllocator);
AK::IAkPluginParam * CreateMyPluginParams(AK::IAkPluginMemAlloc * in_pAllocator);
AK::PluginRegistration MyPluginRegistration(AkPluginTypeEffect, MY_COMPANY_ID, MY_PLUGIN_ID, CreateMyPlugin, CreateMyPluginParams, MyRegisterCallback, NULL);

有关更多信息,请参阅以下各节: