PaperSloth’s diary

主にゲーム開発関連についての記事を書きます。

XAudio2 で新しいフォルダー (2)を活用する話

概要

この記事は もなふわすい~とる~む Advent Calendar 2020 - Adventar
12/18(18日目)の記事です。

17日目の記事は I am コイケヤ さん による「もなふわアドベントカレンダー17日目。わーい!「各位なかよく小学校」の校章ロゴ作ったよー!\(^o^)/|I AM コイケヤ|note」でした。
校章がとても可愛らしいので何かしらグッズ化されるといいなぁと思います。
他の方の記事も、どれも熱量がすごくいい記事ばかりです!


また、本記事とは別にV沼にハマって狂ったニンゲンのポエムもこちらに公開しました。
papersloth.hatenablog.com
巻乃もなかさんとは?という人はこちらに詳細を書きましたので、目を通していただければです。


本記事では新しいフォルダー (2)を活用し、Visual Studioのビルド時に 巻乃もなか さんのかわいいボイスで開発を応援してもらうツールを作った話です。
本家というかこの記事の元ネタになったのは ねぎぽよ❣️/ニアちゃん👉 (@CST_negi) | Twitter 氏 が公開されているVoiceer というUnity向けのEditor拡張です。
github.com

UE4向けのPluginもぼくが以前にこっそり作りました。
1日で作ったもので出来がかなりアレなので、いずれアップデートをしようと思っています。
papersloth.hatenablog.com

環境

新しいフォルダー (2) とは

新しいフォルダー (2) とは 巻乃もなか さんが公開してくださっている素材用のショートボイス集です。
公開用.zip という名前なのですが、解凍すると新しいフォルダー (2)が展開されます。
現在新しいフォルダー (3)も準備中のようですので、そちらも大変楽しみですね。
※因みに新しいフォルダー、新しいフォルダー (1)は存在しません。
www.fanbox.cc

完成したもの



実装方法について

実装にあたって用意したものは2つです。

  • XAudio2 でサウンドを再生するためのexe
  • ビルドイベントを仕込んだなんかしらのプロジェクト

 今回はむかーしむかし作ったDX12のプロジェクトを引っ張り出してきました。
 C++プロジェクトでもC#プロジェクトでもビルドイベントが仕込めればなんでもいいです。

実装は至って簡単で以前にMicrosoftが公開していたXAudio2 のサンプルを叩き台にします。
現在、XAudio2 のサンプルプロジェクトのリンクが切れているため
土田さんが公開されているXAudio2 のサンプルプロジェクトを使用します。
中身としてはMicrosoftが公開していたサンプルと同様のものになっています。
こちらの書籍のリンクからサンプルがDL可能です(本も良い本ですので、興味がある方は買ってみるといいかもです)
booth.pm

サンプルをDLして展開するとその中にXAudio2BasicSoundというプロジェクトがあります。
その中にあるXAudio2BasicSound.cppとWAVFileReader.h/cpp を利用します。
WAVFileReader.h/cpp についてはDirectXTKに含まれていますので、こちらから最新版をDLして使用することも可能です。
https://github.com/Microsoft/DirectXTK

新規プロジェクトの作成

まずは新規にC++プロジェクトを作成します。
今回作成するのはコンソールアプリケーションなので、テンプレートからコンソールアプリを指定します。
f:id:PaperSloth:20201213142350p:plain

プロジェクト名はXAudioSandboxとしました。
f:id:PaperSloth:20201213142636p:plain

WAVFileReader.h/cppを使用したいため、プロジェクトに追加します。
f:id:PaperSloth:20201213142858p:plain
f:id:PaperSloth:20201213142958p:plain

XAudio2 の初期化

XAudio2 を利用したサウンドの再生にあたって
XAudio2BasicSound.cpp内にあるPlayWave()とFindMediaFileCch() をそのまま流用するため
コピペで持ってきます。

// 中身の解説等は省略
HRESULT PlayWave(_In_ IXAudio2* pXaudio2, _In_z_ LPCWSTR szFilename);
HRESULT FindMediaFileCch(_Out_writes_(cchDest) WCHAR* strDestPath, _In_ int cchDest, _In_z_ LPCWSTR strFilename);

_Use_decl_annotations_
HRESULT PlayWave(IXAudio2* pXaudio2, LPCWSTR szFilename)
{
    // 省略
}

_Use_decl_annotations_
HRESULT FindMediaFileCch(WCHAR* strDestPath, int cchDest, LPCWSTR strFilename)
{
    // 省略
}

次に、XAudio2を利用するための諸々のincludeを追加します。

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <iostream>
#include <xaudio2.h>
#include <string>
#include <wrl/client.h>

#include "WAVFileReader.h"
#pragma comment(lib,"xaudio2.lib")

ここからXAudio2 を使用してwav再生を行っていきますが
ざっくりと流れを説明すると以下のような流れになります。

XAudio2Create() // IXAudio2 の初期化
    ↓
CoInitializeEx() // コンポーネント オブジェクト モデル (COM)の初期化
    ↓
CreateMasteringVoice() // IXAudio2MasteringVoice の初期化
    ↓
PlayWave() // wavの再生 (XAudio2BasicSound.cpp内の関数)
    ↓
DestroyVoice() // IXAudio2MasteringVoice の終了処理
    ↓
CoUninitialize() // COMの終了処理

先ずは先程の流れに沿ってIXAudio2 の初期化処理を追加します。

int main()
{
    Microsoft::WRL::ComPtr<IXAudio2> pXAudio2 = nullptr;
    UINT32 flags = 0;
    HRESULT hr = XAudio2Create(&pXAudio2, flags);

    if (FAILED(hr))
    {
        wprintf(L"Failed to init XAudio2 engine: %#X\n", hr);
        return 1;
    }
    return 0;
}

続いてCOMの初期化、終了処理を追加します。

int main()
{
    // IXAudio2 の初期化
    // ...

    CoInitializeEx(nullptr, COINIT_MULTITHREADED);
    // TODO : wavの再生処理
    CoUninitialize();

    return 0;

TODOの中ではIXAudio2MasteringVoice の初期化とwavの再生とIXAudio2MasteringVoice の終了処理を行っていきます。
まずはIXAudio2MasteringVoice の初期化と終了処理を追加します。

int main()
{
    // IXAudio2 の初期化
    // ...

    CoInitializeEx(nullptr, COINIT_MULTITHREADED);

    // IXAudio2MasteringVoiceの生成と初期化
    IXAudio2MasteringVoice* pMasteringVoice = nullptr;
    if (FAILED(hr = pXAudio2->CreateMasteringVoice(&pMasteringVoice)))
    {
        wprintf(L"Failed creating mastering voice: %#X\n", hr);
        CoUninitialize();
        return 1;
    }
    // IXAudio2MasteringVoiceの破棄
    pMasteringVoice->DestroyVoice();

    CoUninitialize();

    return 0;

ここまででおおまかなXAudioの初期化と終了処理ができました。
あとはPlaySound() を呼んでwavを再生すれば、ひとまずの音声の再生は完了です。

int main()
{
    // IXAudio2 の初期化
    // ...

    CoInitializeEx(nullptr, COINIT_MULTITHREADED);

    // IXAudio2MasteringVoiceの生成と初期化
    ..

    if (FAILED(hr = PlayWave(pXAudio2.Get(), /*file pathの指定*/)))
    {
        wprintf(L"Failed creating source voice: %#X\n", hr);
        CoUninitialize();
        return 1;
    }

    // IXAudio2MasteringVoiceの破棄
    pMasteringVoice->DestroyVoice();

    CoUninitialize();

    return 0;

ボイスの再生

新しいフォルダー (2) に含まれる音声データはmp3ですので、適当なツールを使ってwav に変換しておきます。
変換したwav は以下のdirectory構造で配置しました。

+- Root
    |
    +- XAudioSandbox
    |   |
    |   +- Resources
    |       |
    |       +- Voice
    |           |
    |           +- いいコード書いてる?.wav
    |           +- ...etc
    +- XAudioSandbox.sln

先程のPlayWave内にファイルパスを通してやればひとまずボイスの再生ができることの確認ができるかと思います。

if (FAILED(hr = PlayWave(pXAudio2.Get(), L"Resources\\Voice\\再生したよ~!.wav")))

ここまでのコードを一旦載せておきます。
gist.github.com

次にビルド開始時と終了時で別々のボイスを再生するように処理を分けたいためenumと実行時引数を追加します。
また、実行時の入力引数は1つでよいため指定がない場合や複数指定されている場合はエラーを返すようにします。

enum class BuildType : uint8_t
{
    Start,
    Finish
};

int main(int argc, char* argv[])
{
    if (argc <= 1)
    {
        wprintf(L"The number of arguments is not enough.\n");
        return 1;
    }
    if (argc > 2)
    {
        wprintf(L"There are too many arguments, please specify one argument.\n");
        return 1;
    }

    BuildType type = BuildType::Start;
    std::wstring file_path = L"Resources\\Voice\\";
    std::string option = argv[1];
    if (option.compare("start") == 0)
    {
        // TODO : ビルド開始時の複数ボイスからのランダム再生
        file_path.append(L"いいコード書いてる?.wav");
        type = BuildType::Start;
    }
    else if (option.compare("finish") == 0)
    {
        // TODO : ビルド終了時の複数ボイスからのランダム再生
        file_path.append(L"コンパイルおわったよ~!.wav");
        type = BuildType::Finish;
    }
    else
    {
        wprintf(L"Incorrect argument options.\n");
        return 1;
    }

    // XAudioの処理
    // ...
    return 0;


続いて、複数ボイスの中からランダムなボイスの再生について
まずは乱数の生成を行います。
今回は実装がお手軽なメルセンヌ・ツイスタ(mt19937)を使用します。

#include <random>

// 範囲指定の乱数生成
uint64_t GenerateRandomRange(uint64_t min, uint64_t max)
{
    std::random_device device;
    static std::mt19937_64 mt64(device());

    std::uniform_int_distribution<uint64_t> uniform(min, max);

    return uniform(mt64);
}

// 呼び出しイメージ
// GenerateRandomRange(0, 10)   0-10の乱数を生成する

乱数生成関数はこれだけです。非常にお手軽ですね。

あとは先程のファイルパスの初期化部分で乱数の生成結果に応じて呼び出すファイルを変えてやればランダムなボイスが再生されます。
今回は決め打ちでarrayに突っ込んでいますが、真面目に作るなら特定のdirectory以下のfileからランダム呼び出しを行うなどの変更が必要かなとは思います。

#include <array>

int main(int argc, char* argv[])
{
    ...
    std::array<std::wstring, 5> finishArray
    {
        L"コンパイルおわったよ~!.wav",
        L"コンパイルおわり!その調子!.wav",
        L"コンパイルおわり!順調かな?.wav",
        L"コンパイルおわり!順調かな?2.wav",
        L"コンパイル終わり~!その調子~!.wav",
    };
    ...
    else if (option.compare("finish") == 0)
    {
        type = BuildType::Finish;
        file_path.append(finishArray[GenerateRandomRange(0, finishArray.size() - 1)]);
    }
    ...

呼び出すアプリケーションでの設定

アプリケーション側からの設定はVisual Studioのビルド前イベント、ビルド後イベントにexe のpathと実行時引数を与えるだけです。
ビルド前のイベントにはexe へのパスと start の引数を設定しています。
f:id:PaperSloth:20201213201256p:plain

ビルド後のイベントにはexe へのパスと finish の引数を設定しています。
f:id:PaperSloth:20201213201351p:plain

また、ボイスを入れたResourcesフォルダも実行側のプロジェクトから呼び出し可能なdirectoryにコピーしています。
あとは通常通りビルドを行えば音声が再生されます。

最後にコードの全文を載せておきます。
gist.github.com

まとめ

これでVisual Studioでも推しのボイスを再生する方法がわかりましたね!
UnityであればVoiceerを使ったり、別途Editor拡張から気軽に音声の再生ができるのでいいですね!
UE4であればVoiceer4Uを使うか、Engine側の既存のAudioを直接上書きするかEditor拡張で呼び出せるのでこちらもお手軽ですね!

また、冒頭の動画を見てお気付きになった方もいるかもしれませんが
音声再生 -> 再生終了後にビルド というステップになるため、イテレーションは落ちますw
推しの声が聞けるんだから、それぐらいの時間は我慢してください( ´∀`)

最近のゲームエンジンは喋りますが、最近のIDEもよく喋るようになりましたね!やったね!

明日はニワカ先輩 さんによる記事が公開されますね!どんな内容になるのか楽しみです!

以上です!
f:id:PaperSloth:20201213203808p:plain