はじめに
こんにちは。UbisoftのSnowdropエンジンのオーディオ技術アーキテクト、ロバート・バンタンです。今回はWwise 2021から導入された、よりエレガントであるとも言える新GUIモデルに合わせて、あなたのレガシーWwiseプラグインを更新するためのガイドを書きました。Wwiseは旧モデルとの後方互換性を維持していますが、それが壊れてしまう状況もあります。例えば、新規導入されたタブワークフローに旧モデルが対応しきれていない状況にあり、いずれ使えなくなってしまいます。
Audiokineticの担当チームは、このように述べています:
- Wwise 2023.1.1(または2023.1の別のマイナーバージョン):レガシープラグインAPIを非推奨とし、ユーザを新APIの移行ガイドに導きます
- Wwise 2024.1:レガシープラグインAPIを削除します:https://www.audiokinetic.com/ja/library/edge/?source=SDK&id=whatsnew_2024_1_migration.html
本手順の前提条件
あなたのカスタムWwiseプラグインのソースコードと、Visual Studio(またはほかのIDE)のプロジェクトまたはソリューションファイルが手元にあり、そのしくみを知っていることが条件です。
概要
本記事の執筆時点で、Wwise 2022用に作成可能なWwiseプラグインの典型的なタイプは、以下の3種類です:
- Source
- Effect
- Sink(つまり"Device")
各プラグインのパッケージは、ゲームまたはエンジンに直接リンクされたスタティックライブラリと、Wwiseオーサリングツールのpluginsフォルダにコピーされるダイナミックライブラリ(およびパラメータマップを示すxmlファイル)の2つのデプロイ可能物として提供される必要があります。
GUIはプラグインコンテナから分離されているため、新しい構造ではGUIを複数インスタンス化することができます。
ゴールにたどり着くまでの手順
ステップ1
関数dllのエクスポートを削除します
SourceタイプとEffectタイプの両方でデプロイ可能な.dllは、AkCreatePlugin()メソッドをエクスポートする必要がありました。一方、SinkタイプではAkCreatePlugin()メソッドと、AkGetSinkPluginDevices()メソッドの両方をエクスポートする必要がありました。いずれの場合も、それらを.DEFファイルに記載する方法が一般的でした。
例
LIBRARY "ReallyCoolPlugin" EXPORTS AkCreatePlugin AkGetSinkPluginDevices
これが不要となったため、あなたのソリューションに.DEFファイルがある場合は関数名を削除してください。ほかのエントリーポイントの宣言がない場合、このファイルそのものを削除しても問題ありません。
例
LIBRARY "ReallyCoolPlugin" EXPORTS
ステップ2
すべてのdllブートストラップコードを削除します
通常これは、WwisePluginパスに存在するReallyCoolPlugin.cpp、および.hファイルで宣言および実装されていました。
例
class ReallyCoolPluginApp : public CWinApp
翻訳単位にAkCreatePlugin()の実装が入っていました(Sinkプラグインでは、このほかにAkGetSinkPluginDevices()もありました)。
これについては後ほど、GUIクラスで"AK::Wwise::Plugin::PluginMFCWindows<>"をサブクラス化することにより自動的に処理されるため、これらのファイルは削除できます。
ステップ3
ファクトリーメソッドを実装します
SourceプラグインとEffectプラグインで実装するファクトリーメソッドは同じです。ReallyCoolFX.cppファイルに2個のローカル関数を実装します:
AK::IAkPlugin* CreateReallyCoolFX( AK::IAkPluginMemAlloc * in_pAllocator ) { return AK_PLUGIN_NEW( in_pAllocator, ReallyCoolFX() ); } AK::IAkPluginParam* CreateReallyCoolFXParams(AK::IAkPluginMemAlloc* in_pAllocator) { return AK_PLUGIN_NEW(in_pAllocator, ReallyCoolFXParams()); } AK_IMPLEMENT_PLUGIN_FACTORY(ReallyCoolFX, AkPluginTypeEffect, AKCOMPANYID_YOURSTUDIO, AKEFFECTID_YOURPLUGIN) /*または*/ AK_IMPLEMENT_PLUGIN_FACTORY(ReallyCoolFX, AkPluginTypeSource, AKCOMPANYID_YOURSTUDIO, AKEFFECTID_YOURPLUGIN)
これらのマクロは入力として受け取った関数名を前方宣言するため、マクロをこの関数の実装より上に入れても問題ありません。どちらでも構いません。
一方、Sinkプラグインは以下のシグネチャを用いて、ReallyCoolFX(サウンドエンジンクラス)内のコールバックメソッドとしてAkGetSinkPluginDevices()を再実装する必要があります:
AKRESULT GetReallyCoolDeviceList(AkUInt32& io_maxNumDevices, AkDeviceDescription* out_deviceDescriptions)
現在このコールバックを使用してファクトリークラスを宣言するための、AK_IMPLEMENT_PLUGIN_FACTORYマクロのバージョンがないため、以下のように手作業で実施してください:
AK::PluginRegistration ReallyCoolFXRegistration(AkPluginTypeSink, AKCOMPANYID_YOURSTUDIO, AKEFFECTID_YOURPLUGIN, ReallyCoolFX, ReallyCoolFXParams, GetReallyCoolDeviceList);
ファクトリーヘッダ"ReallyCoolFXFactory.h"ですでに使用しているAK_STATIC_LINK_PLUGIN()マクロが、"ReallyCoolFXRegistration "という関数が出てくる前提で残りのリンカ作業を行うため、正しい命名規則に従いさえすれば、ここでほかに行うべきことはありません。
ステップ4
GUIコードとシリアライズコードを、2つの異なるクラスに分けます
以前はGUIの駆動とSoundBankをシリアライズする時のパラメータのシリアライズを、1つのクラスで行いました。以下のようなインターフェースを使っていたと思います:
#pragma once #include <AK/Wwise/AudioPlugin.h> class ReallyCoolPlugin : public AK::Wwise::DefaultAudioPluginImplementation
あなたはこのデフォルト実装から多くのメソッドをオーバーライドしたかもしませんが、次のpublicメソッドが最低限必要になる可能性が最も高くなります:
void Destroy() override; void SetPluginPropertySet(AK::Wwise::IPluginPropertySet* in_pPSet) override; bool GetBankParameters(const GUID& in_guidPlatform, AK::Wwise::IWriteData* in_pDataWriter) const override; HINSTANCE GetResourceHandle() const override; bool GetDialog(eDialog in_eDialog, UINT& out_uiDialogID, AK::Wwise::PopulateTableItem*& out_pTable) const override; void NotifyPropertyChanged(const GUID&, LPCWSTR in_szPropertyName) override; bool WindowProc(eDialog in_eDialog, HWND in_hWnd, UINT in_message, WPARAM in_wParam, LPARAM in_lParam, LRESULT& out_lResult) override;
新しいモデルにおいてはこの2種類の機能を完全に分けているため、シリアライズのコードのインスタンス化は1回だけとし、GUIは複数回インスタンス化できるようになりました。
Serializationクラス
ReallyCoolPlugin.cppと.hファイルは、シリアライズを処理するだけのクラスに縮小できるようになりました。<AK/Wwise/Plugin.h>ヘッダをインクルードし、AK::Wwise::Plugin::AudioPluginインターフェースをpublicメソッドで継承します:
// ReallyCoolPlugin.h #pragma once #include <AK/Wwise/Plugin.h> extern const char* const szSomeCoolIntParameter; extern const char* const szSomeOtherCoolFloatParameter; extern const char* const szEnableThing; class ReallyCoolPlugin : public AK::Wwise::Plugin::AudioPlugin { public: ReallyCoolPlugin(); bool GetBankParameters(const GUID& in_guidPlatform, AK::Wwise::Plugin::DataWriter& in_dataWriter) const override; }; AK_DECLARE_PLUGIN_CONTAINER(ReallyCool);
ここで2つの点に注目してください。新しいdatawriterはパラメータの文字列IDがLPCWSTRではなく、const char*になった事を除けば、以前のものと非常によく似た動作をしますが、AK_DECLARE_PLUGIN_CONTAINER()と呼ばれる定型コードを実行するためのマクロが追加されました。
このクラスの実装は非常に明快です。
// ReallyCoolPlugin.cpp #include "ReallyCoolPlugin.h" const char* const szSomeCoolIntParameter = "SomeCoolIntParameter"; const char* const szSomeOtherCoolFloatParameter = "SomeOtherCoolFloatParameter"; const char* const szEnableThing = "SomeBoolParameter"; ReallyCoolPlugin::ReallyCoolPlugin() { // ここにデバッグメッセージを追加することも可能。例えばstdafx.hのモーダルダイアログボックス。あるいはデバッグの時だけにコンパイルされるものなど。 } bool ReallyCoolPlugin::GetBankParameters(const GUID& in_guidPlatform, AK::Wwise::Plugin::DataWriter& in_dataWriter) const { in_dataWriter.WriteInt32(m_propertySet.GetInt32(in_guidPlatform, szSomeCoolIntParameter)); in_dataWriter.WriteReal32(m_propertySet.GetReal32(in_guidPlatform, szSomeOtherCoolFloatParameter)); in_dataWriter.WriteBool(m_propertySet.GetBool(in_guidPlatform, szEnableThing)); return true; } AK_DEFINE_PLUGIN_CONTAINER(ReallyCool); AK_EXPORT_PLUGIN_CONTAINER(ReallyCool); AK_ADD_PLUGIN_CLASS_TO_CONTAINER(ReallyCool, ReallyCoolPlugin, ReallyCoolFX); DEFINE_PLUGIN_REGISTER_HOOK; DEFINEDUMMYASSERTHOOK;
ほかのPODタイプ向けのアクセサやシリアライザもあります。すべてが大変論理的になっています。
多くの提携コードがAK_DEFINE_PLUGIN_CONTAINER()とAK_ADD_PLUGIN_CLASS_TO_CONTAINER()によって実装されるだけでなく、.DEFファイルで行われていたdllエクスポート作業もAK_EXPORT_PLUGIN_CONTAINER()で実行されるようになったことに注意してください。
GUIクラス
もうお分かりかもしれませんが、GUIコードがプラグインのシリアライズクラスから取り除かれたため、ほかの場所に別の様式で置く必要があります。それがinclude <AK/Wwise/Plugin.h>であり、あなたがすでにReallyCoolPlugin.hファイルに追加しています。以下は多重継承を通して実装する必要がある、最低限のコードです。
// ReallyCoolPluginGUI.h #pragma once #include "ReallyCoolPlugin.h" // ここに<AK/Wwise/Plugin.h>がすでに含まれている class ReallyCoolPluginGUI final : public AK::Wwise::Plugin::PluginMFCWindows<> , public AK::Wwise::Plugin::GUIWindows , public AK::Wwise::Plugin::RequestHost , public AK::Wwise::Plugin::RequestPropertySet { public: ReallyCoolPluginGUI() {} void NotifyPropertyChanged(const GUID& in_guidPlatform, const char* in_szPropertyName) override; bool GetDialog(AK::Wwise::Plugin::eDialog in_eDialog, UINT& out_uiDialogID, AK::Wwise::Plugin::PopulateTableItem*& out_pTable) const override; bool WindowProc(AK::Wwise::Plugin::eDialog in_eDialog, HWND in_hWnd, UINT in_message, WPARAM in_wParam, LPARAM in_lParam, LRESULT& out_lResult) override; private: HWND myHwndPropView = nullptr; };
GUIの状態をszEnableThingのようなプロパティで変更したい場合は、以下のような形式でprivateメソッドを追加することを検討してください:
private: void PrivateMethodToChangeThingState();
実装上、これら関数のコードは以前とほぼ同じです。ただしタイプが微妙に異なるため、既存のGUIコードを適応させる場合に注意することが大切です。
ほかにも対応が必要な、重要なアーキテクチャ上の変更があります。GUI駆動型のフロントエンドdllパラメータは従来と同様、バックエンドライブラリでロードされるシリアライズ済みパラメータにまだ結合されていますが、GUIの特定インスタンスにのみ影響するメンバ変数をGUIクラスに設けることが可能となりました。これらはバックエンドライブラリでは一切これを使用しない、GUIインスタンスの状態を変更する変数である必要があります。反対にバックエンドlibが使用する予定のパラメータはすべて、どのGUIインスタンスがユーザによって変更されたかに関係なく、フロントエンドdllパラメータとして全GUIインスタンスに伝播される共有状態の一部である必要があります。
AudiokineticのSamuel Longchamps氏はこのように述べています。「バグのよくある原因の1つとして、本来はGUIのすべてのインスタンスにおいて同じであるべき変数が、2つのインスタンスで別のコピーを有していることがあります。代わりに、プラグインの共有状態をバックエンドクラスに置き、バックエンドを介してアクセスするようにしてください。
(クラスをRequestLinkBackendから継承して)LinkBackendサービスをリクエストすることで、フロントエンドのインスタンスに関連付けられたバックエンドのインスタンスにアクセスできます。その逆も、バックエンドクラスでRequestLinkFrontendを継承することにより可能です。フロントエンドのインスタンスが複数存在する可能性があるため、LinkFrontendを使用する場合はすべてのフロントエンドインスタンスにおいて、この処理を繰り返して共有状態と同期させる必要があります。」
以上に加え、AKは“stdafx.h”というプリコンパイルされたヘッダを使用しています(このヘッダ名には歴史的な経緯があります)。このヘッダが<AK/Wwise/TargetVer.h>や、<AK/AkPlatforms.h>などの共通ヘッダを宣言するほか、Windows SDKから宣言される内容に影響を与える、いくつかの定義を設定します(例えば#include<afxwin.h> や、#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORSなど)。
Visual Studioには大昔に非推奨となったMFC GUIデザイナーツールがありますが、これをあなたが使っていると想定すると、GUIウィジェット用の多数のIDの入った自動生成の"resource.h"があるはずです。このヘッダもここに入れてください。
オーサリングツールのスライダによって、置き換えられないGUIウィジェットを更新するreplaceマクロもあります。覚えているかもしれませんが、縦と横のフェーダはMFCテキストコントロールウィジェットとして作成され、"Class=Fader;Prop=SomeXMLDefinedProperty"や、"Class=SuperRange;Prop=SomeOtherXMLDefinedProperty"といったキャプションがつきます。なんとも奇妙で便利な処理でしたが、これはラジオボタンやチェックボックス等のほかのMFCウィジェットに適用されないため、マクロのコレクションを使い、変更がないかをプラグインがリッスンできるプロパティテーブルを定義しました。
以前のマクロ:
AK_BEGIN_POPULATE_TABLE(...) AK_POP_ITEM(...,...) AK_END_POPULATE_TABLE()
新しいマクロ:
AK_WWISE_PLUGIN_GUI_WINDOWS_BEGIN_POPULATE_TABLE(...) AK_WWISE_PLUGIN_GUI_WINDOWS_POP_ITEM(..., ...) AK_WWISE_PLUGIN_GUI_WINDOWS_END_POPULATE_TABLE()
ここまで説明したところで、このGUIの実装を見てみましょう:
// ReallyCoolPluginGUI.cpp #include "stdafx.h" // 必ずこれを最初に置きます #include "ReallyCoolPluginGUI.h" #include "resource.h" AK_WWISE_PLUGIN_GUI_WINDOWS_BEGIN_POPULATE_TABLE(ReallyCoolPluginProp) AK_WWISE_PLUGIN_GUI_WINDOWS_POP_ITEM(IDC_CHECK_ENABLETHING_ID, szEnableThing) // IDC_CHECK_ENABLETHING_IDは、"resource.h"で定義される AK_WWISE_PLUGIN_GUI_WINDOWS_END_POPULATE_TABLE() // プロパティ変更について、必要なアクションを取る // 注:該当するメッセージをWindowProc関数でキャッチするという選択肢もある。 void ReallyCoolPluginGUI::NotifyPropertyChanged(const GUID& /*in_guidPlatform*/, const char* in_szPropertyName) { if (!myHwndPropView) { return; } if (!strcmp(in_szPropertyName, szEnableThing)) { PrivateMethodToChangeThingState(); } } void ReallyCoolPluginGUI::PrivateMethodToChangeThingState() { bool thingEnabled = m_propertySet.GetBool(m_host.GetCurrentPlatform(), szEnableThing); HWND hwndItem = GetDlgItem(myHwndPropView, IDC_THING_ID); // ghost化またはunghost化するMFCウィジェットのハンドルを取得する AKASSERT(hwndItem); // hwndItemがポイントしたMFCウィジェットをghostする、またはunghostする ::EnableWindow(hwndItem, MKBOOL(thingEnabled)); // MKBOOLがC++ boolをMFC BOOLに変換する } UIコントロールバインディングが設定されたテーブルにプロパティ名を設定する。 bool ReallyCoolPluginGUI::GetDialog(AK::Wwise::Plugin::eDialog in_eDialog, UINT& out_uiDialogID, AK::Wwise::Plugin::PopulateTableItem*& out_pTable) const { AKASSERT(in_eDialog == AK::Wwise::Plugin::SettingsDialog); if (in_eDialog == AK::Wwise::Plugin::SettingsDialog) { out_uiDialogID = IDD_REALLYCOOL_PANEL; // IDD_REALLYCOOL_PANELは、"resource.h"で定義される out_pTable = ReallyCoolPluginProp; return true; } return false; } // 標準ウィンドウ関数。ユーザは関係するメッセージをインターセプトしてUI動作を実装できる。 bool ReallyCoolPluginGUI::WindowProc(AK::Wwise::Plugin::eDialog /*in_eDialog*/, HWND in_hWnd, UINT in_message, WPARAM /*in_wParam*/, LPARAM /*in_lParam*/, LRESULT& out_lResult) { switch (in_message) { case WM_INITDIALOG: { myHwndPropView = in_hWnd; break; } case WM_DESTROY: { myHwndPropView = NULL; break; } } out_lResult = 0; return false; } AK_ADD_PLUGIN_CLASS_TO_CONTAINER(ReallyCool, ReallyCoolPluginGUI, ReallyCoolFX); // あなたのGUIクラスを、PluginContainerに追加する。
以上です。迷った時はAKヘッダーを調べ、パラメータの目的を確認してください(かなり詳しい説明がコメントにあります)。
おまけの情報
(?)ボタンをプラグインのドキュメント用に実装する
プラグイン用に作成したカスタムGUIがウィンドウ表示された時、(?)ボタンから関連ドキュメントに飛べるようにしたいものです。Delayのようなバンドルされているエフェクトを参照すると、以下のようになっています:
// ユーザーが"?"アイコンをクリックした時、オンラインヘルプを実装する bool DelayGUI::Help( HWND in_hWnd, AK::Wwise::Plugin::eDialog in_eDialog, const char* in_szLanguageCode ) const { AFX_MANAGE_STATE( ::AfxGetStaticModuleState() ) ; if ( in_eDialog == AK::Wwise::Plugin::SettingsDialog ) ::SendMessage( in_hWnd, WM_AK_PRIVATE_SHOW_HELP_TOPIC, ONLINEHELP::Delay_Properties, 0 ); else return false; return true; }
このコードと同じことを繰り返した場合、アプリケーション固有のウィンドウズメッセージが親ウィンドウにを送信されるという唯一の問題が発生します。あなたはオーサリングツールのソースコードを持っていないため、これをリッスンしているものに同様のメッセージIDを接続することができません。代わりにWindowsのシェルコマンドを使うのはどうでしょう:
HINSTANCE ShellExecuteA( [in, optional] HWND hwnd, [in, optional] LPCSTR lpOperation, [in] LPCSTR lpFile, [in, optional] LPCSTR lpParameters, [in, optional] LPCSTR lpDirectory, [in] INT nShowCmd );
Delayエフェクトと同じように、Help()メソッドを実装する必要があります。それでは、ReallyCoolPluginGUIクラスを宣言するために継承した、1つのクラスの内部を見てみます。
AK::Wwise::Plugin::GUIWindows
Help()という仮想メソッドが確かにありますが、それはスタブです。これをオーバーライドして、あなたのドキュメントを開くための実装を追加してください。
はじめにヘッダファイルにおいて:
public: ... bool Help(HWND in_hWnd, AK::Wwise::Plugin::eDialog in_eDialog, const char* in_szLanguageCode) const override;
次にcppファイルにおいて、AK_ADD_PLUGIN_CLASS_TO_CONTAINERの前で以下のようにします:
// ユーザーが"?"アイコンをクリックした時に、オンラインヘルプを実装する bool ReallyCoolPluginGUI::Help(HWND in_hWnd, AK::Wwise::Plugin::eDialog in_eDialog, const char* in_szLanguageCode) const { AFX_MANAGE_STATE(::AfxGetStaticModuleState()); if (in_eDialog == AK::Wwise::Plugin::SettingsDialog) ShellExecute(0, 0, L"https://confluence.yourcompany.com/x/4YkVoQ", 0, 0, SW_SHOW); else return false; return true; }
このShellExecute()の呼び出しに使用するアドレスは、何にしますか?例えば、あなたのConfluenceに関するドキュメントだとします。ブラウザでそのページを開き、共有ボタンを押します。
コピーボタンを押し、結果のパーマリンクを引用符で囲んだ文字列に貼り付けます。生成されたパーマリンクを埋め込むと、ページ上のタイトルを編集した時にリンクが変更されないため、通常のブラウザパスよりも安全です。
ここであなたのプラグインを再構築し、デプロイします。ユーザが(?)ボタンを押した時に、当該ページがブラウザの新しいウィンドウとして開きます。
Wwiseオーサリングツール上で、ユーザにデバッグメッセージを表示する
すべてが今動作しているとしましょう。しかし待ってください。DLLのユーザーにメッセージを送るためのコードのどこかにWinAPIコールがあり、それにはまだLPWSTR文字列データが必要です。その呼び出しにパラメータ名のconst char*を渡すことができませんが、どのように変換するのでしょう。
具体的な例を紹介します。一部のパラメータを非推奨とするエフェクトプラグインがあり、Wwiseプラグインのxmlファイルが最新でなかった場合、デザイナーが誤ってそれらのパラメータの設定を生成し、SoundBankにシリアライズしてしまうことを防ぎたいと思いました。 これにより、サウンドエンジンライブラリがデバッグビルドでデータを読み込む時に、"check bank data size"のアサートが発生します。おそらくこれに気がつくのはグラフィックプログラマーだけですが、彼らはすぐにビクビクしてしまうので、助けてあげましょう。
非推奨の2つのパラメータは、このように宣言されます:
const char* const szRightInputSource = "RightInputSource"; const char* const szInputSourceszOutputMode = "OutputMode";
次に、標準strcmp()関数を利用して、受信したプロパティの変更がこれらのいずれかであるかどうかを確認します。スタック上に単純なwchar_t配列を作成し、それを使用してMessageBoxEx()関数が(以前と同じように)LPWSTRを介して読み込むことができるプロパティ文字列を一時的に変換します。メッセージボックスもこのスタック上にあり、仮配列よりも先にスコープ外となります。
if (!strcmp(in_szPropertyName, szRightInputSource) || !strcmp(in_szPropertyName, szOutputMode)) { wchar_t wtext[20]; // どのプロパティ名も保持できる充分な大きさとする int wchars_num = MultiByteToWideChar(CP_UTF8, 0, in_szPropertyName, -1, NULL, 0); // wcharの同等の長さを決定する MultiByteToWideChar(CP_UTF8, 0, in_szPropertyName, -1, wtext, wchars_num); // 新しい長さを使用してutf8をwcharに変換する LPWSTR wcptr = wtext; ::MessageBoxEx(nullptr, L"ChannelSwitcherPlugin::NotifyPropertyChanged - property not supported", wcptr, IDOK, 0); }
関連コンテンツ
Audiokineticの参考情報 : https://www.audiokinetic.com/ja/library/edge/?source=SDK&id=effectplugin_tools_newplugin.html
Audiokineticの移行ガイド :https://www.audiokinetic.com/ja/library/edge/?source=SDK&id=plugin_21_1_migration.html
コメント