版本

menu_open

如何创建 Wwise 声音引擎效果器插件

效果器插件接口实现

Effect 效果器插件接收已有的声音作为输入音频数据,将 DSP 算法作用于这些输入数据。编写效果器插件的工作主要包括实现 AK::IAkInPlaceEffectPlugin 或者 AK::IAkOutOfPlaceEffectPlugin 接口中的一个。在此只介绍与这些接口相关的函数。请参阅 如何创建 Wwise 声音引擎插件 了解与其他插件类型(AKIAkPlugin 接口)共享的接口组件的信息。另请参阅随附的 AkDelay 插件了解详情(示例 )。

AK::IAkEffectPlugin::Init()

此方法为效果器插件用于处理数据做准备,分配内存并设置初始条件。

此插件通过指针传递给内存分配器接口(AKIAkPluginMemAlloc)。您应该通过此接口来执行所有动态内存分配,并使用随附的内存分配宏(请参阅在音频插件中分配/取消分配内存 )。对于最常见的内存分配需求,即在初始化时分配内存和在终止时释放内存的情况下,插件将不需要保留指向分配器的指针。在终止时也会将此指针提供给插件。

AK::IAkEffectPluginContext 接口可用于获取信息,例如旁通掉状态或与效果器插件工作环境相关的其他信息。该接口还可以通过 AK::IAkPluginContextBase::GlobalContext() 访问全局上下文。

插件还接收指向其相关参数节点接口(AKIAkPluginParam)的指针。大多数插件希望保留对相关参数节点的引用,以便能够在运行时获取参数。请参阅参数节点与插件之间的通信。 了解更多详情。

所有这些接口将在插件的生命周期内有效,因此必要时在内部引用它们是安全的。

效果器插件还接收输入/输出音频格式(此格式在插件寿命期间保持不变),以便能够为给定的声道配置分配内存和设置处理。

Note.gif
Note: 每次效果器实例化时都会调用 AK::IAkEffectPlugin::Init(),当声部开始播放或者混音总线实例化时,就会发生效果器实例化。典型情况下,其他声音已经在播放,因此实例化需要在合理的时间段内进行。如果您需要初始化通用/全局数据结构,那么在注册插件库时就应该这样做。请参阅在插件中使用全局声音引擎回调 了解更多详情。

AK::IAkPluginEffect::Execute()

效果器插件可选择实现以下 2 个接口之一:AKIAkInPlaceEffectPlugin或AKIAkOutOfPlaceEffectPlugin。一般大多数效果器应该采用原地效果(它们对输入和输出数据使用相同的音频缓冲区)。然而,当数据流有变时(例如 time-stretching 时间伸缩效果器),则必须实现非原地接口。

Caution.gif
Caution: 具有不同输入/输出声道配置的非原地效果器可插入 Master-Mixer hierarchy(主混音器层级结构)中。然而,无法将变速率效果器放置在混音总线上。具有不同输入/输出缓冲长度的效果器只可插入 Actor-Mixer hierarchy(用作源效果器)。

IAkInPlaceEffectPlugin::Execute

此方法原地执行给定音频缓冲区上的插件信号处理算法(请参阅访问使用 AkAudioBuffer 结构的数据 了解详情)。此结构向插件提供有关输入采样点中有多少点有效(AkAudioBufferuValidFrames)和缓冲区可容纳的最大音频采样帧数(AkAudioBufferMaxFrames() method)的信息。AkAudioBuffereState结构成员向插件指示"这是不是最后一次执行":AK_NoMoreData 表示"是",AK_DataReady 表示"不是"。

当虚声部从已用时间播放(Play from Elapsed Time)时,AKIAkInPlaceEffectPlugin::TimeSkip() 将替代 Execute(),以便插件能够在需要的情况下保持更新它们的内部状态。

IAkOutOfPlaceEffectPlugin::Execute

此方法对非原地算法执行插件的信号处理。使用两个 AkAudioBuffer 结构,一个用于输入缓冲区,另一个用于输出缓冲区。管线使用输出音频缓冲区的 eState 来确定效果器的当前状态。只有当效果器在以下两种情况之一下才需要返回:一是处理完输入缓冲区时(会返回 AK_DataNeeded,以便稍后处理更多的数据),二是在填满整个输出缓冲区时(返回 AK_DataReady)。在非原地效果中还可实现效果器尾音,方法是将收到的 AK_NoMoreData 更改为 AK_DataReady,直至效果器排空其内部状态(此时应返回 AK_NoMoreData)。

直接处理完整个输入缓冲区,管线才会释放输入缓冲区。因此,一定要使用 in_uInOffset 编置参数开始读取上次 Execute() 调用停止时未读完的数据。下面示例介绍如何实现这一点。

void CAkSimpleUpsampler::Execute(   
        AkAudioBuffer * io_pInBuffer, 
        AkUInt32        in_uInOffset,
        AkAudioBuffer * io_pOutBuffer )
{
    assert( io_pInBuffer->NumChannels() == io_pOutBuffer->NumChannels() );
    const AkUInt32 uNumChannels = io_pInBuffer->NumChannels();
    AkUInt32 uFramesConsumed; // 跟踪处理了输入数据中的多少数据
    AkUInt32 uFramesProduced; // 跟踪在输出缓冲区中产生了多少数据
    for ( AkUInt32 i = 0; i < uNumChannels; i++ )
    {
        AkReal32 * AK_RESTRICT pInBuf = (AkReal32 * AK_RESTRICT) io_pInBuffer->GetChannel( i ) + in_uInOffset; 
        AkReal32 * AK_RESTRICT pfOutBuf = (AkReal32 * AK_RESTRICT) io_pOutBuffer->GetChannel( i ) + io_pOutBuffer->uValidFrames;
        uFramesConsumed = 0; // 针对各个声道进行复位
        uFramesProduced = 0; 
        while ( (uFramesConsumed < io_pInBuffer->uValidFrames) && (uFramesProduced < io_pOutBuffer->MaxFrames()) )
        {
            // 执行一些处理,以不同的速率处理输入并产生输出(例如时间伸缩效果器或重采样)
            *pfOutBuf++ = *pInBuf;
            *pfOutBuf++ = *pInBuf++;
            uFramesConsumed++;
            uFramesProduced += 2;
        }
    }
    // 更新 AkAudioBuffer 结构以继续进行处理
    io_pInBuffer->uValidFrames -= uFramesConsumed;
    io_pOutBuffer->uValidFrames += uFramesProduced;
    if ( io_pInBuffer->eState == AK_NoMoreData && io_pInBuffer->uValidFrames == 0 )
        io_pOutBuffer->eState = AK_NoMoreData; // 输入全部处理完毕,没有更多可输出,效果器已执行完毕
    else if ( io_pOutBuffer->uValidFrames == io_pOutBuffer->MaxFrames() )
        io_pOutBuffer->eState = AK_DataReady; // 整个音频缓冲区准备就绪
    else
        io_pOutBuffer->eState = AK_DataNeeded; // 我们需要更多数据来继续处理
}

当虚声部采用 Play from Elapsed Time 时,AKIAkOutOfPlaceEffectPlugin::TimeSkip() 将替代 Execute(),以便插件能够在需要的情况下能一直更新其内部状态。然后此函数负责告诉管线,产生给定数量的输出帧通常需要处理多少输入采样点。

Implementing 效果器插件尾音

某些效果器具有内部状态,在输入完成播放后,必须输出该内部状态,以便正确进行衰减,最典型的是带延时线(delay line)的效果器。效果器 API 使得即使在没有任何有效输入数据的情况下也可继续执行。当 AkAudioBuffer 结构的 eState 标志变成 AK_NoMoreData 时,在完成当前执行后,管线不会再将有效的输入采样帧送到插件。然后插件可以自由在缓冲区中写入新帧(最大帧数为 MaxFrames() 返回的值),以便在完成输入信号后清空延时线。应始终告诉音频管线已经输出了多少帧,方法是正确更新 uValidFrames 字段。如果需要再次调用插件 Execute() 函数来完成效果器尾音刷新,则应将 eState 成员应为 AK_DataReady。只有当效果器为 eState 字段设置了 AK_NoMoreData 时,管线才会停止调用插件 Execute()。

处理尾音的最简单方法是使用 SDK 中提供的 AkFXTailHandler 服务类。由于一份 AkFXTailHandler 用作插件的类成员,因此我们在原位效果器中唯一需要做的是调用 AkFXTailHandler::HandleTail(),并将它和输出完成时要输出的音频样本总数(根据参数的不同,各个执行之间存在差异)传输给 AkAudioBuffer。请参阅 AkDelay 插件源代码了解详情(示例 )。

有关执行效果器插件的重要注意事项

  • 在 Wwise 中使用插件时,无论参数是否与 RTPC 关联,参数变化都会发送至参数节点。如果使用 Wwise 时需要,这可以让插件支持在运行时更改非 RTPC 参数的值。如果不希望插件支持此功能,应在初始化时复制这些参数值,确保它们在插件的存续期间保持不变。
  • 插件应处理多个声道配置(如果可以插入到总线上,至少有单声道、立体声和 5.1),或者在初始化时返回 AK_UnsupportedChannelConfig。

旁通

在 Wwise 或游戏中可通过各种机制(例如 UI、事件和 RTPC)旁通插件。在这些情况下,不会调用插件 Execute() 函数。当解除旁通的时候,插件继续运行并再次调用 Execute() 函数时,插件将重新启动它的处理工作。旁通时将调用插件的 Reset() 函数,以清除延时线和其他状态信息,从而在最终解除旁通时能够有一个全新的开始。请参阅AK::IAkPlugin::Reset() 了解详情。

Caution.gif
Caution: 运行时旁通和解除旁通插件可能导致信号中断,具体取决于插件和被处理的声音素材。
AKRESULT CAkGain::Init( AK::IAkPluginMemAlloc * in_pAllocator,      // 内存分配器接口。
                        AK::IAkEffectPluginContext * in_pFXCtx,     // FX 环境。
                        AK::IAkPluginParam * in_pParams,            // 效果器参数。
                        AkAudioFormat & in_rFormat                  // 必需的音频输入格式。
                         )
{
    if ( in_pFXCtx->IsSendModeEffect() )
        // 环境(已发送)环境中使用的效果器……   
    else
        // DSP 链中插入的效果器……
}

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

发布 Wwise 插件的监控数据

在用于监控的时候,插件可能需要将信息发回到 Wwise。常见例子有发送有关音频信号的信息(如 VU 电平表)、运行时信息比如内存占用等等。

在通过性能分析系统异步发送数据前,效果器应该首先确定是否可通过调用 AK::IAkEffectPluginContext::CanPostMonitorData() 将数据发送到此效果器插件的对应 UI。 要做到这点,您需要将指针缓存到插件执行环境(execution context)中,该执行环境会在在效果器初始化时交给插件。 注意,只有插件位于总线上时才可发送数据,因为在这种情况下,它与其效果器设置视图呈一对一的关系。

如果数据可以发送并且构建目标版本可与 Wwise 通信(即非 Release 版本),则您可以发布任何大小的数据块。您可以通过 AK::IAkEffectPluginContext::PostMonitorData() 来按照您的喜好组织数据块。监控数据一旦发布,您就可以安心地放弃插件中的数据块了。

void MyPlugin::Execute( AkAudioBuffer * io_pBuffer )
{
    // 算法跟踪以下浮点数组 fChannelPeaks[MAX_NUM_CHANNELS] 中所有 m_uNumChannels 个声道的信号峰值;
    ...
#ifndef AK_OPTIMIZED
    if ( m_pCtx->CanPostMonitorData() )
    {
        unsigned int uMonitorDataSize = sizeof( unsigned int ) * m_uNumChannels*sizeof(float); 
        char * pMonitorData = (char *) AkAlloca( uMonitorDataSize );
        *((unsigned int *) pMonitorData ) = m_uNumChannels;
        memcpy( pMonitorData+sizeof(unsigned int), fChannelPeaks, m_uNumChannels*sizeof(float) );
        m_pCtx->PostMonitorData( pMonitorData, uMonitorDataSize );
        // 现在可释放 pMonitorData
    }
#endif
    ...
}

此页面对您是否有帮助?

需要技术支持?

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

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

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

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

开始 Wwise 之旅