高效地整合音乐
在使用 Wwise 整合游戏音乐时,要播放整首音乐才能确认其循环是否自然。比如,若有段两分钟的音乐,要确保循环点之间无缝衔接,得花整整两分钟从头听到尾。
同样,在检查互动音乐中的段落变换和过渡时也是如此。尤其对于长音轨末尾附近的过渡,没问题的话预览一次可能就够了。要是有任何问题,修正设置和素材之后还得重新播放整条音轨。
这种反复检查的过程耗时又费力,时间紧张的话可能会影响到品质。
为了解决这一问题,PlatinumGames 开发了独有的工具 Music Render。藉此,借助 Wwise 的离线渲染 API,高效地检查音频循环和过渡。在此,我们就来介绍一下该工具的使用示例和技术配置。
Music Render:使用示例
下面以 Wwise 示例工程 Cube 为例展示了如何利用 Music Render 有效解决在通过 Wwise 整合音乐时常见的一些问题。
示例 1:简单的循环
"Story" Music Playlist Container 是个只包含一个 Music Segment 的简单循环。它的时长约为一分钟。也就是说,检查循环需要一分钟的时间。
这个时候就要用到 Music Render 了。为此,可从 Wwise Playlist 菜单激活该工具并设置以下参数。
- Rendering time:检查循环需要多长时间(1 ~ 10分钟)
- Application to open the WAV file:比如,MAGIX 的 SOUND FORGE。
单击 [Start Rendering] 可创建 SoundBank 并自动实施渲染。在渲染完成后,会在 SOUND FORGE 中打开 WAV 文件。然后,便可从循环点快速播放 Music Segment 来检查循环。
示例 2:复杂的播放列表
"Explore" Music Playlist Container 包含多个 Music Segment。假如没有工具,就要花很长时间播放整个 Music Segment 来检查其过渡是否存在问题。
在渲染过程中,Music Render 会在生成的波形文件中自动为段落过渡点添加标记。这样不用听完整首乐曲就可检查过渡;它们会从这些标记位置接着往后播放。
示例 3:互动过渡
"Wwise 201 Music" Music Switch Container 会将音轨切换为 Combat 或 Explore(由 "Gameplay_Switch" Switch 触发)。
在检查过渡时通常要手动切换音轨,但 Music Render 提供更高效的方法,其允许在创建和渲染 Event 时自动切换。
通过配置以下各项创建示例 Event:
- 播放 Switch Container
- 每 15 秒更改 Switch 值
在通过 Music Render 对 Event 做离线渲染时会创建音乐波形,并以 15 秒为间隔(15、30、45…)切换音轨。若发现不自然的过渡,可对过渡或素材进行修改,然后重新渲染来确认修复效果。这种自动化操作可确保即便对于难以手动再现的问题也能快速检查并加以修正。
技术配置
Music Render 由前端和后端组成。
前端 (UI)
- 编程语言:C#
- UI 框架:WPF
- 启动:作为附加组件 (.exe) 在 Wwise 设计工具中启动
- 功能:
- 通过 WAAPI 与 Wwise 设计工具进行通信
- 创建 Event 和 SoundBank
- 向后端发送渲染命令,并在渲染完成后打开 WAV 文件
后端(渲染流程)
- 编程语言:C++
- 启动:从前端启动 (.dll)
- 功能:
- 通过 Wwise SDK 初始化声音引擎
- 加载 SoundBank、发送 Event 并通过离线渲染来输出音频
- 在完成整个流程后将结果返回给前端
后端细节
有关初始化 Wwise 声音引擎和加载 SoundBank 等基本流程,请参阅官方文档。本节总结了离线渲染需要特别注意的点。不妨了解一下。
所需全局变量
AK::OfflineRendering::g_fFrameTimeInSeconds = 1.0f / 60.0f;
AK::OfflineRendering::g_bOfflineRenderingEnabled = true;
这些变量在 AkProfile.cpp 中定义。虽然通常要有相应 Wwise 授权才能访问源代码,但创建自用的离线渲染工具时应该不需要授权。
离线渲染 API
AK::SoundEngine::StartOutputCapture(settings.WavFileName);
AK::SoundEngine::StopOutputCapture();
通过回调添加标记
static void MusicCallback(AkCallbackType in_eType, AkCallbackInfo* in_pCallbackInfo)
{
AK::SoundEngine::AddOutputCaptureMarker("Entry");
}
整段代码
下面贴出了主要的代码段落。其中包含 MusicRender 类以及该类中使用的结构定义。另外,还包含离线渲染所需的所有技术点,不过去掉了错误处理部分。
// MusicRender.h
struct MusicRenderSettings{
wchar_t* SoundBankFolderName;
wchar_t* StreamFolderName;
wchar_t* SoundBankFileName;
wchar_t* PluginDllFolderName;
wchar_t* Event;
int RenderingDurationSec;
wchar_t* WavFileName;
};
typedef void(__stdcall* NotifyProgress)(float);
extern "C" __declspec(dllexport) void __stdcall MusicRender(const MusicRenderSettings&, const NotifyProgress);
// MusicRender.cpp
namespace AK {
namespace OfflineRendering
{
extern AkReal32 g_fFrameTimeInSeconds;
extern bool g_bOfflineRenderingEnabled;
}
}
CAkFilePackageLowLevelIODeferred g_lowLevelIO;
class CMusicRender
{
public:
const AkGameObjectID LISTENER_ID = 0;
const AkGameObjectID GAME_OBJECT_MUSIC = 1;
AkPlayingID m_iPlayingID = 0;
void Initialize(const MusicRenderSettings& settings)
{
AK::SoundEngine::RegisterGameObj(LISTENER_ID, "Listener (Default)");
AK::SoundEngine::SetDefaultListeners(&LISTENER_ID, 1);
g_lowLevelIO.SetBasePath(settings.SoundBankFolderName);
AK::StreamMgr::SetCurrentLanguage(AKTEXT("English(US)"));
AkBankID bankID;
AK::SoundEngine::LoadBank("Init.bnk", bankID);
AK::SoundEngine::LoadBank(settings.SoundBankFileName, bankID);
AK::SoundEngine::RegisterGameObj(GAME_OBJECT_MUSIC, "Music");
g_lowLevelIO.SetBasePath(settings.StreamFolderName);
}
void Terminate(const MusicRenderSettings& settings)
{
AK::SoundEngine::StopPlayingID(m_iPlayingID);
AK::SoundEngine::UnregisterGameObj(GAME_OBJECT_MUSIC);
AK::SoundEngine::UnloadBank(settings.SoundBankFileName, NULL);
AK::SoundEngine::UnloadBank("Init.bnk", NULL);
}
void Main(const MusicRenderSettings& settings, const NotifyProgress callback)
{
AK::OfflineRendering::g_fFrameTimeInSeconds = 1.0f / 60.0f;
AK::OfflineRendering::g_bOfflineRenderingEnabled = true;
AK::SoundEngine::StartOutputCapture(settings.WavFileName);
{
m_iPlayingID = AK::SoundEngine::PostEvent(settings.Event, GAME_OBJECT_MUSIC, AK_MusicSyncEntry, MusicCallback);
const int callbackIntervalMS = 10;
DWORD lastNotificationTimeMS = GetTickCount64();
float renderTimeSec = settings.RenderingDurationSec;
float audioBufferSec = AK::OfflineRendering::g_fFrameTimeInSeconds;
for (float timeSec = 0; timeSec < renderTimeSec; timeSec += audioBufferSec) {
AK::SoundEngine::RenderAudio();
if (GetTickCount64() - lastNotificationTimeMS >= callbackIntervalMS) {
callback(timeSec / renderTimeSec);
lastNotificationTimeMS = GetTickCount64();
}
}
}
AK::OfflineRendering::g_bOfflineRenderingEnabled = false;
AK::SoundEngine::StopOutputCapture();
}
static void MusicCallback(AkCallbackType in_eType, AkCallbackInfo* in_pCallbackInfo)
{
AK::SoundEngine::AddOutputCaptureMarker("Entry");
}
};
void __stdcall MusicRender(const MusicRenderSettings& settings, const NotifyProgress callback)
{
if (InitSoundEngine(settings) == false) {
return;
}
CMusicRender cMusicRender;
cMusicRender.Initialize(settings);
cMusicRender.Main(settings, callback);
cMusicRender.Terminate(settings);
TermSoundEngine();
}
结语
从大概 2019 年开始,我们在好几个 PlatinumGames 项目中都使用了这一工具。它让我们可以快速检查音乐循环和互动过渡,并在此基础上创作更加复杂精细的音乐作品。借助此工具,您可以轻松完成各项检查,腾出时间来探索如何在游戏中呈现音乐。
另外,若在开发阶段被要求提供声音素材,还可轻松将分割成段落的 WAV 文件合并,并输出为单个 WAV 文件来用作循环素材。因为素材是按照游戏中的音量渲染的,所以跟只包含音效的演示视频合并时,两者的音量自然而然就可以达到平衡。对此,该工具还允许同时渲染多个播放列表容器和 Event。
希望这篇博文能帮助各位更加高效地整合音乐!
评论