音楽実装を効率よく
Wwiseを使ったゲーム音楽の実装では、「音楽のループが自然につながっているかどうか」を確認するために、実時間で音楽を再生する必要があります。たとえば、2分間の音楽であれば、実際に2分間聴いて、ループポイントが滑らかかどうかを確かめなければいけません。
インタラクティブ音楽におけるセグメント遷移やトランジションの確認も同様で、特に長い曲の終盤に発生する遷移では、問題がなければ1回で済みますが、何か不具合があれば設定やアセットの修正後に再度確認が必要です。
こうした確認作業は繰り返しが多く、時間的にも精神的にも負担となりがちです。時間が限られているときは、妥協したチェックになってしまうことも少なくありません。
そこでプラチナゲームズでは、WwiseのオフラインレンダリングAPIを活用して、音楽のループや遷移をより効率的に確認できるツール「Music Render」を独自開発しました。この記事ではそのツールの使用例と技術的な構成についてご紹介します。
Music Render使用例
Wwiseを使った音楽実装でよく発生する課題に対し、Music Render を使ってどう効率的に対応できるかを、具体的な使用例を通して紹介します。ここでは Wwise のサンプルプロジェクト『Cube』 を使用しています。
使用例 1:シンプルなループ
Storyプレイリストコンテナは、1つのミュージックセグメントのみで構成されたシンプルなループです。長さは約1分あり、ループを確認するにはそのまま1分聴く必要があります。
このような場合にMusic Renderが活躍します。Wwiseのプレイリストメニューからツールを起動し、以下のパラメータを設定します:
- レンダリング時間:ループ確認に必要な長さ(1〜10分で指定)
- WAVファイルを開くアプリ:例として MAGIX 社の SOUND FORGE を指定
「レンダリング開始」をクリックするとサウンドバンクの生成とレンダリングが自動的に行われ、完了後にWAVファイルがSOUND FORGEで開きます。ユーザはループポイントから再生して、すばやく確認できます。
使用例 2:複雑なプレイリスト
Explore プレイリストコンテナは複数のミュージックセグメントを組み合わせた構成です。それぞれの遷移に問題がないかを確認するには、やはり実時間再生が必要です。
Music Renderでレンダリングすると、生成された波形ファイルにはセグメントの遷移位置にマーカーが自動的に追加されます。ユーザはこのマーカー位置をピックアップして再生するだけで、全体を聞かずとも遷移の確認が可能です。
使用例 3:インタラクティブ遷移
Wwise 201 Music というスイッチコンテナは、スイッチ Gameplay_Switch によって Combat または Explore の音楽へと切り替わるしくみになっています。
この遷移を確認するには、通常は手動でスイッチを切り替えながら再生確認しますが、Music Renderではこれを自動化したイベントを作成してレンダリングすることで効率的に検証できます。
具体的には、以下のようなイベントを作成します:
- スイッチコンテナの再生
- 15秒ごとにスイッチ値を変更
このイベントをMusic Renderでオフラインレンダリングすれば、15秒、30秒、45秒…のタイミングで切り替わる音楽の波形が生成されます。不自然な遷移があった場合はトランジションやアセットを修正し、再レンダリングで確認すればOKです。手動確認ではタイミング的に再現が難しい問題があっても、自動化によって確実に修正を確認できます。
技術構成
Music Renderはフロントエンドとバックエンドで構成されています。
フロントエンド(UI)
- 実装言語:C#
- UIフレームワーク:WPF
- 起動方式:Wwiseオーサリングツールのアドオンとして起動(.exe)
- 機能:
- WAAPI経由でWwiseオーサリングツールと通信
- イベントの作成、サウンドバンクの生成
- バックエンドへのレンダリング指示、完了後にWAVファイルを開く
バックエンド(レンダリング処理)
- 実装言語:C++
- 起動方式:フロントエンドから起動(.dll)
- 機能:
- Wwise SDKを使用してサウンドエンジンを初期化
- サウンドバンクのロード、イベントのポスト、オフラインレンダリングによる音声出力
- 処理完了後にフロントエンドへ結果を返す
バックエンド詳細
Wwiseサウンドエンジンの初期化、バンクロードなどの基本処理は公式ドキュメントに準じています。ここではオフラインレンダリングに特有のポイントを整理します:
設定すべきグローバル変数:
AK::OfflineRendering::g_fFrameTimeInSeconds = 1.0f / 60.0f;
AK::OfflineRendering::g_bOfflineRenderingEnabled = true;
これらの変数は AkProfile.cpp に定義されています。ソースコードの参照にはライセンスが必要な場合がありますが、参照できなくてもツールの作成自体にはおそらく問題はないはずです。
オフラインレンダリング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年ごろからプラチナゲームズの複数タイトルで活用しています。音楽のループやインタラクティブな遷移の確認を効率化することで、単なる作業時間の短縮にとどまらず、より高度な音楽演出を支える基盤となっています。確認作業をすばやく終えられることで、表現の工夫や試行錯誤に時間を充てることができます。
また副次的な用途として、開発中に音楽素材を求められた際に、セグメントとして複数のWAVファイルに分かれたアセットを連結してレンダリングし、ループ素材として簡単に出力できます。素材はゲーム内の音量でレンダリングされるため、効果音のみを収録したプレイ動画との組み合わせたとき、音量バランスが自然に整う利点もあります。こうした用途も考慮して、複数のプレイリストコンテナやイベントをまとめてバッチレンダリングする機能も搭載しています。
この記事が、音楽実装の効率化に取り組むヒントになれば幸いです!
コメント