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")

但是这样的修改会造成同时支持 armv7 和 arm64 时具有链接错误,解决方案看下节。

Android 支持 armv7 和 arm64

当在 UE 的项目设置中为 Android 同时支持 arm64armb7时,上面的修改会具有链接错误,需要变动 WWise 中链接库的路径,具体的方法可以看我这篇笔记:同时支持 armv7 和 arm64 的链接库

Android 链接库的拷贝

WWise 的 SDK 中同时使用 UPL 和 RuntimeDependencies 添加了链接库,造成了重复,所以可以把 RuntimeDependencies 中针对移动平台去掉:

1
2
3
4
5
6
7
8
9
10
11
12
public AkAudio(ReadOnlyTargetRules Target) : base(Target)
{
// ...
if (Target.Platform == UnrealTargetPlatform.Win64)
{
foreach(var RuntimeDependency in AkUEPlatformInstance.GetRuntimeDependencies())
{
RuntimeDependencies.Add(RuntimeDependency);
}
}
// ...
}

远程构建 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:指定生成平台保存到的目录

命令行参数

  • -nosound:关闭 WWise 的声音。
1
2
3
4
5
6
7
8
9
bool FAkAudioDevice::EnsureInitialized()
{
// We don't want sound in those cases.
if (FParse::Param(FCommandLine::Get(), TEXT("nosound")) || FApp::IsBenchmarking() || IsRunningDedicatedServer() || IsRunningCommandlet())
{
return false;
}
// ...
}

全局暂停音乐

可以通过以下代码实现:

1
2
3
4
5
6
// on enter backgraound
AK::SoundEngine::Suspend(false);

// on enter foreground
AK::SoundEngine::WakeupFromSuspend();
AK::SoundEngine::RenderAudio();

热更 WWise 的 Bank

根据上面的分析可以看到, WWise 在生成 Bank 时以平台为单位生成,每个平台都会生成对应的文件夹。
这要求我们在热更时需要根据不同的平台包含不同的外部文件,HotPatcher已经支持了这个特性,可以实现特定平台包含特殊的文件。

WWise 集成至 UE 的缺点

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

文档