UE集成WWise:概念与代码分析

WWise是Audiokinetic的跨平台音频引擎,可以与游戏引擎很好地进行交互,负责音频的同事可以只在WWise中处理音频,把游戏业务和音频的制作与管理分离,提供事件和参数给游戏引擎使用,实现与业务的解耦和对音频更精确的控制。
本篇文章主要介绍WWise与UE4的集成、远程构建、资源分析、文档收录,WWise与UE的控制交互以及Bank生成的代码分析。

集成至UE4

WWise是全平台支持的,对Linux/Lumin/PS4/Switch/XboxOne/Windows/Android/iOS/Mac都支持。
但是多数游戏不需要支持这么多平台,WWise链接库很大,所以我在官方版本支持的多平台基础上做了裁剪,去掉了以下平台的支持:

  • Linux
  • Lumin
  • PS4
  • Switch
  • XboxOne

对Android和Windows平台做了以下裁剪:

  • 移除arm64-v8a和android_x86/x86_64的链接库支持
  • 移除Win32的所有链接库/移除vc140/vc150的支持

对iOS做了以下裁剪:

  • 移除所有的iphonesimulator,节省空间2.31G

对Mac的支持:

目前的项目是不需要支持Mac的(使用iOS远程出包),但是为了避免想要在Mac上跑工程编译不过的问题,保留了Mac链接库和模块支持,保留它不会对Android/iOS的打包有任何影响。

链接库

我在裁剪版本中支持以下平台:

  • Android_armabi_v7a
  • iOS
  • Mac
  • Win vc160

每个平台均支持Debug/Profile/Release的支持,分别对应UE的Debug/Development/Shipping的Configuration配置。

在打包Android Development的配置下,包含WWise的链接库,APK增大约30M.

WWise版本的问题

Wwise版本为Wwise 2019.1.9.7221

AkAudio_Android.build.cs中对Android的的链接库支持在UE_4_25_OR_LATER下路径错误。

原始路径:

1
Path.Combine(ThirdPartyFolder, "Android", "armeabi-v7a", akConfigurationDir, "lib")

实际的路径:

1
Path.Combine(ThirdPartyFolder, "Android_armeabi-v7a", akConfigurationDir, "lib")

远程构建iOS

在我之前的笔记中写到过,远程构建iOS实际就是要把文件上传的Mac上执行编译,但是这就有一个问题,如果需要参与编译的文件没有被上传到Mac上,就会出现错误,很不巧在WWise中就会出现这个问题,解决的办法自然是要把编译WWise依赖的文件给上传到Mac上。

因为UE使用RSync来同步构建机和本地的文件传输,在我之前的文章UE4开发笔记:Mac/iOS篇#配置远程构建有讲到,可以创建<ProjectDir>/Build/Rsync/RsyncProject.txt文件,来写入RSync的文件同步规则,把需要的文件上传到Mac中。

WWise需要的规则如下:

1
2
+ /Plugins/Wwise/ThirdParty/include/**
+ /Plugins/Wwise/ThirdParty/iOS/**

其实就是指定WWise的链接库和Include目录全上传到Mac上。

WWise资源

WWise在UE中有两种资源格式,一种是UAkAudioEvent用来执行WWise中指定的Event,还有一种是UAkAudioBank,用来记录Bank中包含哪些Event,用在生成时标记把属于相同Bank的Event打包到一起。

UAkAudioEvent

UAkAudioEvent:主要作用是指定当前Event的Bank,它唯一的函数就是LoadBank来加载当前Event所指定的Bank(看到有用到的地方就是获取它的名字),WWise集成到UE的插件也是通过拿到UAkAudioEvent对象的名字来与UAkAudioBank做绑定之后去WWise端生成bnk等文件的,这也是要求UE中资源的命名要和WWise中Event的命名完全一致的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
UCLASS(meta=(BlueprintSpawnableComponent))
class AKAUDIO_API UAkAudioEvent : public UObject
{
GENERATED_UCLASS_BODY()

public:
/** Bank to which this event should be added. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bank")
class UAkAudioBank * RequiredBank;

/** Maximum attenuation radius for this event */
UPROPERTY(BlueprintReadOnly, Category="AkAudioEvent")
float MaxAttenuationRadius;

/** Whether this event is infinite (looping) or finite (duration parameters are valid) */
UPROPERTY(BlueprintReadOnly, Category = "AkAudioEvent")
bool IsInfinite;

/** Minimum duration */
UPROPERTY(BlueprintReadOnly, Category = "AkAudioEvent")
float MinimumDuration;

/** Maximum duration */
UPROPERTY(BlueprintReadOnly, Category = "AkAudioEvent")
float MaximumDuration;

#if CPP
/**
* Load the required bank.
*
* @return true if the bank was loaded, otherwise false
*/
bool LoadBank();
#endif

};

通过获取Event的名字再传递给更深层次的PostEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int32 UAkGameplayStatics::PostEvent(
class UAkAudioEvent* AkEvent
, class AActor* Actor
, int32 CallbackMask
, const FOnAkPostEventCallback& PostEventCallback
, const TArray<FAkExternalSourceInfo>& ExternalSources
, bool bStopWhenAttachedToDestroyed
, FString EventName
)
{
if (AkEvent == NULL && EventName.IsEmpty())
{
UE_LOG(LogScript, Warning, TEXT("UAkGameplayStatics::PostEvent: No Event specified!"));
return AK_INVALID_PLAYING_ID;
}

if (Actor == NULL)
{
UE_LOG(LogScript, Warning, TEXT("UAkGameplayStatics::PostEvent: NULL Actor specified!"));
return AK_INVALID_PLAYING_ID;
}

AkDeviceAndWorld DeviceAndWorld(Actor);
if (DeviceAndWorld.IsValid())
{
AkCallbackType AkCallbackMask = AkCallbackTypeHelpers::GetCallbackMaskFromBlueprintMask(CallbackMask);
if (ExternalSources.Num() > 0)
{
FAkSDKExtrernalSourceArray SDKExternalSrcInfo(ExternalSources);
return DeviceAndWorld.AkAudioDevice->PostEvent(GET_AK_EVENT_NAME(AkEvent, EventName), Actor, PostEventCallback, AkCallbackMask, false, SDKExternalSrcInfo.ExternalSourceArray);
}
else
{
return DeviceAndWorld.AkAudioDevice->PostEvent(GET_AK_EVENT_NAME(AkEvent, EventName), Actor, PostEventCallback, AkCallbackMask);
}
}

return AK_INVALID_PLAYING_ID;
}

UAkAudioBank

UAkAudioBank的作用是用来调用AkAudioDevice来加载Bank,如果在UE中开启了AutoLoad,则在UObejct的PostLoad中就会去执行加载。

官方的对于SoundBank的介绍:SoundBank

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Called after load process is complete.
*/
void UAkAudioBank::PostLoad()
{
Super::PostLoad();
if ( AutoLoad && !HasAnyFlags(RF_ClassDefaultObject))
{
Load();
}
}

/**
* Loads an AkBank.
*
* @return Returns true if the laod was successful, otherwise false
*/
bool UAkAudioBank::Load()
{
if (!IsRunningCommandlet())
{
FAkAudioDevice * AudioDevice = FAkAudioDevice::Get();
if (AudioDevice)
{
AkBankID BankID;
AKRESULT eResult = AudioDevice->LoadBank(this, AK_DEFAULT_POOL_ID, BankID);
return (eResult == AK_Success) ? true : false;
}
}

return false;
}

而且LoadBank我看到使用的也是和AkAudioEvent类似,也是通过获取它的名字传递给FAkAudioDevices

Bank的生成分析

那么AkAudioEvent是如何与SoundBank进行关联起来的呢?因为我在代码里只看到使用之前需要LoadBank,但是没有看到在UE资源里SounkBnak和Event进行关联起来的地方,而且,SoundBank的命名与在WWise中也没有关系。

答案就在通过AkSoundBank生成的文件上,在对AkSoundBank资源进行Generate Selected SoundBank时:

会在项目设置中的WwiseSoundBankFolder目录下创建出以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\WWise\WwiseDemoGame\Content\WwiseAudio\Windows>tree /a /f
D:.
| Init.bnk
| Init.json
| Init.txt
| Init.xml
| PluginInfo.json
| PluginInfo.xml
| SoundbanksInfo.json
| SoundbanksInfo.xml
| SwitchBank.bnk
| SwitchBank.json
| SwitchBank.txt
| SwitchBank.xml
| VelocityBank.bnk
| VelocityBank.json
| VelocityBank.txt
| VelocityBank.xml

其中Init.*相关的四个文件是必备的Init的Bank的内容。每个Bank都会生成.bnk/.json/.txt/.xml四个文件,UE加载bank需要用到的就是.bnk文件,经过测试,删掉其他的几个文件也没什么问题。

  • bnk:数据文件,*.Bnk可以存储事件的详细信息、音频、其他插件所需要的数据结构。可以简单理解为是一个 数据存放的容器。该文件可以在运行中自由的控制加、卸载。
  • json:描述文件,用于记录当前的bank中有哪些数据、哪些Event等等,以及对应的Event在WWise工程中的路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
"SoundBanksInfo": {
"Platform": "Windows",
"BasePlatform": "Windows",
"SchemaVersion": "11",
"SoundbankVersion": "134",
"RootPaths": {
"ProjectRoot": "C:\\Users\\lipengzha\\Desktop\\WWise\\WwiseDemoGame\\UnrealWwiseDemo\\",
"SourceFilesRoot": "C:\\Users\\lipengzha\\Desktop\\WWise\\WwiseDemoGame\\UnrealWwiseDemo\\.cache\\Windows\\",
"SoundBanksRoot": "C:\\Users\\lipengzha\\Desktop\\WWise\\WwiseDemoGame\\Content\\WwiseAudio\\Windows\\",
"ExternalSourcesInputFile": "",
"ExternalSourcesOutputRoot": "C:\\Users\\lipengzha\\Desktop\\WWise\\WwiseDemoGame\\UnrealWwiseDemo\\GeneratedSoundBanks\\Windows"
},
"SoundBanks": [
{
"Id": "2001541346",
"Language": "SFX",
"ObjectPath": "\\SoundBanks\\Default Work Unit\\VelocityBank",
"ShortName": "VelocityBank",
"Path": "VelocityBank.bnk",
"IncludedEvents": [
{
"Id": "2099597577",
"Name": "PlayRederenceSoundTest",
"ObjectPath": "\\Events\\Default Work Unit\\PlayRederenceSoundTest",
"DurationType": "OneShot",
"DurationMin": "1.338833",
"DurationMax": "1.338833"
},
{
"Id": "3368745218",
"Name": "VelocityLoop",
"ObjectPath": "\\Events\\Default Work Unit\\VelocityLoop",
"DurationType": "Infinite"
}
],
"IncludedMemoryFiles": [
{
"Id": "386490851",
"Language": "SFX",
"ShortName": "Shotgun_Fire_01.wav",
"Path": "SFX\\Shotgun_Fire_01_A07A4AEB.wem"
}
]
}
]
}
}
  • xml:描述文件,与json表示的内容相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<SoundBanksInfo Platform="Windows" BasePlatform="Windows" SchemaVersion="11" SoundbankVersion="134">
<RootPaths>
<ProjectRoot>C:\Users\lipengzha\Desktop\WWise\WwiseDemoGame\UnrealWwiseDemo\</ProjectRoot>
<SourceFilesRoot>C:\Users\lipengzha\Desktop\WWise\WwiseDemoGame\UnrealWwiseDemo\.cache\Windows\</SourceFilesRoot>
<SoundBanksRoot>C:\Users\lipengzha\Desktop\WWise\WwiseDemoGame\Content\WwiseAudio\Windows\</SoundBanksRoot>
<ExternalSourcesInputFile></ExternalSourcesInputFile>
<ExternalSourcesOutputRoot>C:\Users\lipengzha\Desktop\WWise\WwiseDemoGame\UnrealWwiseDemo\GeneratedSoundBanks\Windows</ExternalSourcesOutputRoot>
</RootPaths>
<SoundBanks>
<SoundBank Id="2001541346" Language="SFX">
<ObjectPath>\SoundBanks\Default Work Unit\VelocityBank</ObjectPath>
<ShortName>VelocityBank</ShortName>
<Path>VelocityBank.bnk</Path>
<IncludedEvents>
<Event Id="2099597577" Name="PlayRederenceSoundTest" ObjectPath="\Events\Default Work Unit\PlayRederenceSoundTest" DurationType="OneShot" DurationMin="1.338833" DurationMax="1.338833"/>
<Event Id="3368745218" Name="VelocityLoop" ObjectPath="\Events\Default Work Unit\VelocityLoop" DurationType="Infinite"/>
</IncludedEvents>
<IncludedMemoryFiles>
<File Id="386490851" Language="SFX">
<ShortName>Shotgun_Fire_01.wav</ShortName>
<Path>SFX\Shotgun_Fire_01_A07A4AEB.wem</Path>
</File>
</IncludedMemoryFiles>
</SoundBank>
</SoundBanks>
</SoundBanksInfo>

  • txt:也是描述文件,但和json和xml中的内容相比少了一些(不过基础的Event ID、EventName、)。
1
2
3
4
5
6
7
8
9
10
11
12
Event	ID	Name			Wwise Object Path	Notes
2099597577 PlayRederenceSoundTest \Default Work Unit\PlayRederenceSoundTest
3368745218 VelocityLoop \Default Work Unit\VelocityLoop

Game Parameter ID Name Wwise Object Path Notes
3519441192 Velocity \Default Work Unit\Velocity

Source plug-ins ID Name Type Wwise Object Path Notes
778067245 Wwise Tone Generator Wwise Tone Generator \Actor-Mixer Hierarchy\RTPCDemo\VelocityLoop\Wwise Tone Generator

In Memory Audio ID Name Audio source file Wwise Object Path Notes Data Size
386490851 RederenceSound C:\Users\lipengzha\Desktop\WWise\WwiseDemoGame\UnrealWwiseDemo\.cache\Windows\SFX\Shotgun_Fire_01_A07A4AEB.wem \Actor-Mixer Hierarchy\Default Work Unit\RederenceSound 257120

UAkGameplayStatics中的类似Post*传递Actor的作用是获取World,并且在从该Actor上获取AkComponent(若没有就创建,并Attach到该Actor的RootComponent上):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct AkDeviceAndWorld
{
FAkAudioDevice* AkAudioDevice;
UWorld* CurrentWorld;

AkDeviceAndWorld(AActor* in_pActor) :
AkAudioDevice(FAkAudioDevice::Get()),
CurrentWorld(in_pActor ? in_pActor->GetWorld() : nullptr)
{}

AkDeviceAndWorld(UObject* in_pWorldContextObject) :
AkAudioDevice(FAkAudioDevice::Get()),
#if UE_4_17_OR_LATER
CurrentWorld(GEngine->GetWorldFromContextObject(in_pWorldContextObject, EGetWorldErrorMode::ReturnNull))
#else
CurrentWorld(GEngine->GetWorldFromContextObject(in_pWorldContextObject))
#endif // UE_4_17_OR_LATER
{}

bool IsValid() const { return (CurrentWorld && CurrentWorld->AllowAudioPlayback() && AkAudioDevice); }
};

通过Event来指定SoundBank,然后对SoundBank执行Generated Selected Bank,流程如下:

  1. 根据所选择的SoundBank得到SoundBank的名字列表
  2. 获取引擎中所有的UAkAudioEvent对象,通过分析AkSoundEvent中RequireBank的名字与第一步中获取的SoundBank中的名字是否匹配,从而得到SoundBank中所有的AkAudioEvent
  3. 根据以上两步分析的结果生成SoundBankDefinitionFile,存储在UE的项目目录下,名字为TempDifinitionFile.txt
1
2
3
4
5
6
7
8
9
10
11
VelocityBank	"PlayRederenceSoundTest"
VelocityBank "VelocityLoop"
ExtSrcBnk "Play_MyExtSrc"
AmbientBank "AmbientNoise_NotSpatialized"
AmbientBank "AmbientNoise_Spatialized"
ReverbBank "Fire_Weapon"
MatineeBank "Closed_Hi_Hat"
MatineeBank "Kick"
MatineeBank "Snare"
SubtitleBank "Play_Subtitles"
SwitchBank "Play_Tone"

里面记录了UE里的SoundBank对象与其关联的Event以及Bus的名字,通过-ImportDefinitionFile参数传递给WwiseCLI.exe,从而让WWise端知道UE侧SoundBank和Event之间的对应关系。

  1. WWise端根据传入进来的ImportDefinitionFile文件,根据WWise工程里的Event以及Bus等匹配,产生出来包含指定Event的SoundBank,并生成json/xml/txt等描述文件。

UE通过LoadBank的加载流程应该是:

  1. 通过Bank对象拿到Bank的名字
  2. 根据Bank名字去WwiseSoundBankFolder目录下查找同名的bnk文件
  3. 拿到bnk文件,进行加载

UE与WWise的交互

在UE中可以使用WWise的API来播放和控制声音的一些介绍。

PlaySoundAtLocation

API均在UAkGameplayStatics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** Posts a Wwise Event at the specified location. This is a fire and forget sound, created on a temporary Wwise Game Object. Replication is also not handled at this point.
* @param AkEvent - Wwise Event to post.
* @param Location - Location from which to post the Wwise Event.
* @param Orientation - Orientation of the event.
*/
UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="Audiokinetic", meta=(WorldContext="WorldContextObject", AdvancedDisplay = "3"))
static int32 PostEventAtLocation(class UAkAudioEvent* AkEvent, FVector Location, FRotator Orientation, const FString& EventName, UObject* WorldContextObject );

/** Posts a Wwise Event by name at the specified location. This is a fire and forget sound, created on a temporary Wwise Game Object. Replication is also not handled at this point.
* @param AkEvent - Wwise Event to post.
* @param Location - Location from which to post the Wwise Event.
* @param Orientation - Orientation of the event.
*/
UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="Audiokinetic", meta=(WorldContext="WorldContextObject", DeprecatedFunction, DeprecationMessage = "Please use the \"Event Name\" field of PostEventAtLocation"))
static void PostEventAtLocationByName(const FString& EventName, FVector Location, FRotator Orientation, UObject* WorldContextObject );

这两个API可以通过传递AkAudioEvent的资源或者Event的名字来在指定的位置播放声音。PostEventAtLocationByNameUAkGameplatStatics::PostEventAtLocation的封装版本,AkAudioEvent为NULL。

PostEvent

发送给定 Actor 的根组件绑定和控制的 Wwise Event。如果传递进来的Actor上没有挂载AkComponent,会在PostEvent上创建出来一个组件,并默认Attach到该Actor的RootComponent。

在PostEvent之后,如果后续需要控制Event的值,如RTPC或者Switch等,要传入对应的Actor(因为PostEvent本质上是要通过AkComponent):

1
2
3
4
5
6
7
8
9
10
11
12
AkPlayingID FAkAudioDevice::PostEvent(
const FString& in_EventName,
UAkComponent* in_pComponent,
const FOnAkPostEventCallback& PostEventCallback,
AkUInt32 in_uFlags, /*= 0*/
const TArray<AkExternalSourceInfo>& in_ExternalSources /*= TArray<AkExternalSourceInfo>()*/
)
{
return PostEvent(in_EventName, in_pComponent, in_ExternalSources, [PostEventCallback, in_uFlags, this](AkGameObjectID gameObjID) {
return CallbackManager->CreateCallbackPackage(PostEventCallback, in_uFlags, gameObjID);
});
}

目前我的理解为:传入进去的Actor(上的AkComponent)是WWise播放声音的上下文,通过这个上下文可以在另外的操作中去控制指定的Event.

而PostEvent还具有多种类型的回调函数,控制其的方式是传递进去的Mask值,它由EAkCallbackType枚举值控制(bitmask):

1
2
3
4
5
6
7
static int32 PostEvent(	class UAkAudioEvent* AkEvent, 
class AActor* Actor,
UPARAM(meta = (Bitmask, BitmaskEnum = EAkCallbackType)) int32 CallbackMask,
const FOnAkPostEventCallback& PostEventCallback,
const TArray<FAkExternalSourceInfo>& ExternalSources,
bool bStopWhenAttachedToDestroyed = false,
FString EventName = FString(""));

EAkCallbackType的可选值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// Type of callback. Used as a bitfield in methods AK::SoundEngine::PostEvent() and AK::SoundEngine::DynamicSequence::Open().
UENUM(BlueprintType, meta = (Bitmask))
enum class EAkCallbackType : uint8
{
EndOfEvent = 0 UMETA(ToolTip = "Callback triggered when reaching the end of an event. AkCallbackInfo can be cast to AkEventCallbackInfo."),
Marker = 2 UMETA(ToolTip = "Callback triggered when encountering a marker during playback. AkCallbackInfo can be cast to AkMarkerCallbackInfo."),
Duration = 3 UMETA(ToolTip = "Callback triggered when the duration of the sound is known by the sound engine. AkCallbackInfo can be cast to AkDurationCallbackInfo."),

Starvation = 5 UMETA(ToolTip = "Callback triggered when playback skips a frame due to stream starvation. AkCallbackInfo can be cast to AkEventCallbackInfo."),

MusicPlayStarted = 7 UMETA(ToolTip = "Callback triggered when a Play or Seek command has been executed (Seek commands are issued from AK::SoundEngine::SeekOnEvent()). Applies to objects of the Interactive-Music Hierarchy only. AkCallbackInfo can be cast to AkEventCallbackInfo."),

MusicSyncBeat = 8 UMETA(ToolTip = "Enable notifications on Music Beat. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),
MusicSyncBar = 9 UMETA(ToolTip = "Enable notifications on Music Bar. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),
MusicSyncEntry = 10 UMETA(ToolTip = "Enable notifications on Music Entry Cue. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),
MusicSyncExit = 11 UMETA(ToolTip = "Enable notifications on Music Exit Cue. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),
MusicSyncGrid = 12 UMETA(ToolTip = "Enable notifications on Music Grid. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),
MusicSyncUserCue = 13 UMETA(ToolTip = "Enable notifications on Music Custom Cue. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),
MusicSyncPoint = 14 UMETA(ToolTip = "Enable notifications on Music switch transition synchronization point. AkCallbackInfo can be cast to AkMusicSyncCallbackInfo."),

MIDIEvent = 16 UMETA(ToolTip = "Enable notifications for MIDI events. AkCallbackInfo can be cast to AkMIDIEventCallbackInfo."),
};
  • EndOfEvent:事件结束时触发回调;能够转换到AkEventCallbackInfo
  • Marker:遇到标记时触发回调;能够转换到AkMarkerCallbackInfo
  • Duration:当Sound Engine知道当前声音的持续时间时触发回调;能够转换到AkDurationCallbackInfo
  • Starvation:当播放由于流不足而跳过下一帧时触发回调;能够转换到AkEventCallbackInfo
  • MusicPlayStarted:当执行Play或者Seek时触发(从AK::SoundEngine::SeekOnEvent发出搜索命令),只适用于交互音乐层次结构的对象。能够转换到AkEventCallbackInfo
  • MusicSyncBeat:在Music beat上启用通知;能够转换到AkMusicSyncCallbackInfo
  • MusicSyncBar:在Music Bar上启用通知;能够转换到AkMusicSyncCallbackInfo
  • MusicSyncEntry:在Music Entry上启用通知;能够转换到AkMusicSyncCallbackInfo
  • MusicSyncExit:在Music Exit上启用通知;能够转换到AkMusicSyncCallbackInfo
  • MusicSyncGrid:在Music Grid上启用通知;能够转换到AkMusicSyncCallbackInfo
  • MusicSyncPoint:在Music switch transition synchronization point上启用通知;能够转换到AkMusicSyncCallbackInfo
  • MIDIEvent:为MIDI事件启用通知;能够转换到AkMIDIEventCallbackInfo

Marker

在WWise编辑器中添加的Sound VFX里添加的Audio可以通过编辑来添加Marker,Marker可以在UE调用UAkGameplayStatics::PostEvent时将CallbakMask参数设置为EAkCallbackType::Marker,这样可以在回调函数中接收每次播放到WWise中添加Marker的位置,用于在游戏逻辑中做一些事情,比如显示音频对应的字幕。

UE中监听:

而且Identifier也是从0开始的:

WWise中添加Marker:

在UE里PostEvent时可以把CallbackMask的值添加上Marker,就可以在绑定的事件中接收播放过程中每次遇到的Marker了。可以用在播放的过程中监听显示字幕。

RTPC

Real-time Parameter Controls(实时参数控制,RTPC)用于根据游戏中发生的实时参数变化,实时控制各种 Wwise 对象(包括音效对象、容器、总线、效果器等)的特定属性。

RTPC可以由程序端传递值给WWiese端,WWise端可以根据 传入的值控制声音。

在WWise中RTPC:

Switch

使用Switch可以在引擎中切换不同的模式,比如玩家走在不同地面上的声音。

PostEvent一个AkEvent之后,可以通过SetSwitch传入SwitchGroupSwitchState以及PostEvent时传递的Actor.

Sequencer

WWise提供了在Sequence中使用的支持,可以在Sequence中使用AkAudioEvenesAkAudioRTPC,从而实现在Sequence中来播放和控制Event.

Animation

并且可以使用在动画通知中:

给该通知指定Event:

CommandLet

WWise提供了UE中的Commandlet,用于处理批量生成Bank的功能。

使用介绍:

1
2
3
4
5
6
7
8
Commandlet allowing to generate Wwise SoundBanks.
Usage: <Editor.exe> <path_to_uproject> -run=GenerateSoundBanks [-platforms=listOfPlatforms] [-banks=listOfBanks] [-wwiseCliPath=pathToWwiseCli]
Parameters:
- platforms: (Optional) Comma separated list of platforms for which SoundBanks will be generated, as specified in the Wwise project. If not specified, SoundBanks will be generated for all platforms.
- banks: (Optional) Comma separated list of SoundBanks to generate. Bank names must correspond to a UAkAudioBank asset in the project. If now specified, all SoundBanks found in project will be generated.
- wwiseCliPath: (Optional) Full path to the Wwise command-line application to use to generate the SoundBanks. If not specified, the path found in the Wwise settings will be used.
- help: (Optional) Print this help message. This will quit the commandlet immediately.
For more information, see https://www.audiokinetic.com/library/edge/?source=UE4&id=using_features_generatecommandlet.html

生成SoundBank的Commandle为:

1
Engine/Binaries/Win64/UE4Editor.exe "D:/WwiseDemoGame.uproject" -run=GenerateSoundBanks -platforms=Windows,Android,iOS -wait

-wait参数是我加在CommandLet中的,可以用来控制执行完毕后等待用户输入,避免执行窗口一闪而过的情况。
当没有指定生成哪些SoundBank时,会把项目中所有定义的SoundBank生成,如果想要自己指定,则可以使用-banks=aaa,bbb,ccc等形式来指定。

最终的执行命令为(可以看到-ImportDefinitionFile参数):

1
"C:\Program Files (x86)\Audiokinetic\Wwise 2019.1.9.7221\Authoring\x64\Release\bin\WwiseCLI.exe"  "D:/WwiseDemoGame/UnrealWwiseDemo/UnrealDemo.wproj" -GenerateSoundBanks -Bank ExtSrcBnk -Bank AmbientBank -Bank ReverbBank -Bank VelocityBank -Bank MatineeBank -Bank SubtitleBank -Bank SwitchBank -ImportDefinitionFile "D:/WwiseDemoGame/TempDefinitionFile.txt" -Platform Windows -SoundBankPath Windows "D:\WwiseDemoGame\Content\WwiseAudio\Windows" -Platform Android -SoundBankPath Android "D:\WwiseDemoGame\Content\WwiseAudio\Android" -Platform iOS -SoundBankPath iOS "D:\WwiseDemoGame\Content\WwiseAudio\iOS"

拆分命令来看:

  1. WWiseCLI.exe的路径
  2. -GenerateSoundBanks:传递给WWiseCLI.exe标识用于生成SoundBank
  3. -Bank:指定Bank的名字
  4. -ImportDefinitionFile:指定TempDefinitionFile.txt文件
  5. -Platform:指定平台
  6. -SoundBankPath:指定生成平台保存到的目录

热更WWise的Bank

根据上面的分析可以看到, WWise在生成Bank时以平台为单位生成,每个平台都会生成对应的文件夹。
这要求我们在热更时需要根据不同的平台包含不同的外部文件。目前HotPatcher不支持这一点,准备找时间增加这个功能。

WWise集成至UE的缺点

官方提供的Event导入和指定Bank的流程太繁琐了,每个Event都需要拖到Content Browser里才可以创建出UE的Event资源,然后还需要打开手动指定RequireBank,十分的麻烦。其实WWise编辑器中本身包含了Bank的编辑功能,但是在UE中官方没有提供方法可以批量地生成UE里的Event和Bank资源,这个可以作为业余扩展开发的点,自己搞一个批量导入的功能,不过是后话了,有时间再来搞。

文档

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:UE集成WWise:概念与代码分析
文章作者:查利鹏
发布时间:2020年09月12日 15时31分
本文字数:本文一共有5.9k字
原始链接:https://imzlp.me/posts/9809/
专栏链接:https://zhuanlan.zhihu.com/p/242255510
许可协议: CC BY-NC-SA 4.0
捐赠BTC:1CbUgUDkMdy6YRmjPJyq1hzfcpf2n36avm
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!