DXライブラリでSingletonパターンの音声管理システムを作ってみよう

# この記事について
DXライブラリには音声の統括管理を行う機能はありません。そのため、一般的なゲームに搭載されているサウンドオプションを自力実装する必要があります。
典型的なデザインパターン「Singletonパターン」を用いて音声管理システムを作ってみましょう。
# Singletonパターンとは？
詳細は他の記事を参照してもらうものとして、大まかに説明すると「実体が一つしかないことを保証する構造」となります。
例えばクラスをまたいで作動するクラスを作りたいとき、それぞれのクラスに参照を渡したりするのは非効率です。そんな場合にSingletonパターンを用いると、クラスをまたいで色々なところで呼び出すことができるようになります。

# 仕様
BGM、SE、ボイスの3つに分けてそれぞれの音声調整や一括停止ができるようにします。
SEどうしの重複再生はいったん不可能とします。

## 1.サウンド再生のためのクラスを作成
まずは１つのサウンドハンドルを作成、保持し再生、停止、音量調整を行うクラスを作成します(これ自体はSingletonである必要はありません)。

```cpp:SoundHandle
// VisualStudioの設定からC++言語水準を17以上に上げてください
#include <algorithm>
#include <string_view>

class SoundHandle {
private://変数
	static constexpr int s_InvalidID = -1;	// 無効時のハンドルに代入するID
	int m_SoundHandle{ s_InvalidID };		// 保持しておくサウンドハンドル
public://コンストラクタ、デストラクタ
	SoundHandle() noexcept {}
	~SoundHandle(void) noexcept { Dispose(); }
public://削除するコンストラクタ
	SoundHandle(const SoundHandle&) = delete;
	SoundHandle& operator=(const SoundHandle&) = delete;
	SoundHandle(SoundHandle&&) = delete;
	SoundHandle& operator=(SoundHandle&&) = delete;
public:
	//生ハンドルを取得
	int GetHandle(void) const noexcept { return this->m_SoundHandle; }
	//ハンドルが有効な状態かどうか指定
	bool IsActive(void) const noexcept { return (this->m_SoundHandle != s_InvalidID); }
	//再生中かどうかを指定
	bool IsPlay(void) const noexcept { return (DxLib::CheckSoundMem(m_SoundHandle) == TRUE); }
	//総再生時間を取得
	LONGLONG GetTotalTIme(void) const noexcept { return DxLib::GetSoundTotalTime(m_SoundHandle); }
	//ボリュームを0~255で取得
	int GetVol(void) const noexcept { return DxLib::GetVolumeSoundMem2(m_SoundHandle); }
public:
	//読み込み、複製
	void Load(std::string_view FileName, int BufferNum = 3) noexcept {
		Dispose();		//すでに持っているものがないかチェック
		this->m_SoundHandle = DxLib::LoadSoundMemWithStrLen(FileName.data(), FileName.length(), BufferNum);
	}
	void Duplicate(const SoundHandle& o) noexcept {
		Dispose();		//すでに持っているものがないかチェック
		this->m_SoundHandle = DxLib::DuplicateSoundMem(o.m_SoundHandle);
	}
	//破棄
	void Dispose(void) noexcept {
		if (IsActive()) {
			DeleteSoundMem(this->m_SoundHandle);
			this->m_SoundHandle = s_InvalidID;
		}
	}
	//再生、停止
	void Play(int type = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) const noexcept {
		if (IsActive()) {
			DxLib::PlaySoundMem(m_SoundHandle, type, TopPositionFlag ? 1 : 0);
		}
	}
	void Stop(void) const noexcept {
		if (IsActive()) {
			DxLib::StopSoundMem(m_SoundHandle);
		}
	}
	//ボリューム指定
	void UpdateVolume(int vol) noexcept {
		if (IsActive()) {
			DxLib::ChangeVolumeSoundMem(std::clamp<int>(vol, 0, 255), m_SoundHandle);
		}
	}
};
```

## 2.音声管理システムを作成
単体の音声を管理するシステムを作成します。複数の音声をIDで管理できるようにしています(これ自体もSingletonである必要はありません)。

```cpp:SoundPool
#include<vector>
#include<memory>

class SoundPool {
private:
	std::vector<std::shared_ptr<std::pair<int, std::shared_ptr<SoundHandle>>>> m_handle;
public://コンストラクタ、デストラクタ
	SoundPool(void) noexcept {}
	~SoundPool(void) noexcept { DisposeAll(); }
public:
	SoundPool(const SoundPool&) = delete;
	SoundPool(SoundPool&& o) = delete;
	SoundPool& operator=(const SoundPool&) = delete;
	SoundPool& operator=(SoundPool&& o) = delete;
private:
	const std::shared_ptr<std::pair<int, std::shared_ptr<SoundHandle>>>& FindByID(int ID_t) noexcept {
		for (auto& h : this->m_handle) {
			if (h->first == ID_t) {
				return h;
			}
		}
		return nullptr;
	}
public:
	void			Add(int ID_t, std::string path_t = "") noexcept {
		if (path_t == "") { return; }//読み込めない
		//既存のもので同じIDのものがあればそちらに再設定
		auto& h = FindByID(ID_t);
		if (h) {
			//もとからあるものを破棄して
			h->second->Dispose();
			//再設定
			h->first = ID_t;
			h->second->Load(path_t);
			return;
		}
		//新規に作成して
		this->m_handle.emplace_back(std::make_shared<std::pair<int, std::shared_ptr<SoundHandle>>>());
		this->m_handle.back()->second = std::make_shared<SoundHandle>();
		//設定
		this->m_handle.back()->first = ID_t;
		this->m_handle.back()->second->Load(path_t);
	}
	void			Dispose(int ID_t) noexcept {
		for (auto& h : this->m_handle) {
			if (h->first == ID_t) {
				h->second->Dispose();
				//削除
				std::swap(h, this->m_handle.back());
				this->m_handle.pop_back();
				break;
			}
		}
	}
	void			DisposeAll() noexcept {
		this->m_handle.clear();
	}
public:
	//特定のサウンドを再生
	void			Play(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
		auto& h = FindByID(ID_t);
		if (h) {
			h->second->Play(type_t, TopPositionFlag);
		}
	}
	//特定のサウンドを停止
	void			Stop(int ID_t) noexcept {
		auto& h = FindByID(ID_t);
		if (h) {
			h->second->Stop();
		}
	}
	//すべてのサウンドを止める
	void			StopAll() noexcept {
		for (auto& h : this->m_handle) {
			h->second->Stop();
		}
	}
	//ボリュームを反映
	void			SetVolAll(int vol) noexcept {
		for (auto& h : this->m_handle) {
			h->second->UpdateVolume(vol);
		}
	}
};
```

## 3.Singletonパターンを付与できる基底クラスを作成
こちらは以下リンク先のものをほぼコピーしたものとなります。
https://cflat-inc.hatenablog.com/entry/2014/03/04/214608

大事なのは
・コンストラクタ、デストラクタをprivateとする
・自分のstaticポインタを宣言
・Createで作成しInstanceでポインタを取得する
点です。
InstanceをCreateする前に呼んではいけない為、ポップアップを出して警告ののち終了させています。
継承クラスでは自分のstaticポインタの実体を定義する必要がありますのでそちらも注意！

```cpp:SingletonBase
template <class T>
class SingletonBase {
private:
	static const T* m_Singleton;
public:
	static void Create(void) noexcept {
		m_Singleton = new T();
	}
	static T* Instance(void) noexcept {
		if (m_Singleton == nullptr) {
			MessageBox(NULL, "Failed Instance Create", "", MB_OK);
			exit(-1);
		}
		//if (m_Singleton == nullptr) { m_Singleton = new T(); }
		return (T*)m_Singleton;
	}
protected:
	SingletonBase(void) noexcept {}
	virtual ~SingletonBase(void) noexcept {}
private:
	SingletonBase(const SingletonBase&) = delete;
	SingletonBase& operator=(const SingletonBase&) = delete;
	SingletonBase(SingletonBase&&) = delete;
	SingletonBase& operator=(SingletonBase&&) = delete;
};
//子のサンプル
/*
class A : public SingletonBase<A> {
private:
	friend class SingletonBase<A>;
private:
}
//*/
```


## 4.SingletonBaseを用いて外部から呼び出せるクラスを作る
BGM、SE、ボイスの3つをもちつつ、SingletonBaseを継承したクラスを作ります。

```cpp:SoundSingleton
class SoundSingleton : public SingletonBase<SoundSingleton> {
private:
	friend class SingletonBase<SoundSingleton>;
private:
	std::shared_ptr<SoundPool> m_BGM;
	std::shared_ptr<SoundPool> m_SE;
	std::shared_ptr<SoundPool> m_VOICE;
private://コンストラクタ、デストラクタ
	SoundSingleton(void) noexcept {
		m_BGM = std::make_shared<SoundPool>();
		m_SE = std::make_shared<SoundPool>();
		m_VOICE = std::make_shared<SoundPool>();
	}
	virtual ~SoundSingleton(void) noexcept {}
private:
	SoundSingleton(const SoundSingleton&) = delete;
	SoundSingleton(SoundSingleton&& o) = delete;
	SoundSingleton& operator=(const SoundSingleton&) = delete;
	SoundSingleton& operator=(SoundSingleton&& o) = delete;
public://BGM
	void			AddBGM(int ID_t, std::string path_t = "") noexcept { m_BGM->Add(ID_t, path_t); }
	void			DisposeBGM(int ID_t) noexcept { m_BGM->Dispose(ID_t); }
	void			DisposeBGMAll() noexcept { m_BGM->DisposeAll(); }
	//特定のサウンドを再生
	void			PlayBGM(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
		m_BGM->Play(ID_t, type_t, TopPositionFlag);
	}
	//特定のサウンドを停止
	void			StopBGM(int ID_t) noexcept { m_BGM->Stop(ID_t); }
	//すべてのサウンドを止める
	void			StopBGMAll() noexcept { m_BGM->StopAll(); }
	//ボリュームを反映
	void			SetVolBGMAll(int vol) noexcept { m_BGM->SetVolAll(vol); }
public://SE
	void			AddSE(int ID_t, std::string path_t = "") noexcept { m_SE->Add(ID_t, path_t); }
	void			DisposeSE(int ID_t) noexcept { m_SE->Dispose(ID_t); }
	void			DisposeSEAll() noexcept { m_SE->DisposeAll(); }
	//特定のサウンドを再生
	void			PlaySE(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
		m_SE->Play(ID_t, type_t, TopPositionFlag);
	}
	//特定のサウンドを停止
	void			StopSE(int ID_t) noexcept { m_SE->Stop(ID_t); }
	//すべてのサウンドを止める
	void			StopSEAll() noexcept { m_SE->StopAll(); }
	//ボリュームを反映
	void			SetVolSEAll(int vol) noexcept { m_SE->SetVolAll(vol); }
public://VOICE
	void			AddVOICE(int ID_t, std::string path_t = "") noexcept { m_VOICE->Add(ID_t, path_t); }
	void			DisposeVOICE(int ID_t) noexcept { m_VOICE->Dispose(ID_t); }
	void			DisposeVOICEAll() noexcept { m_VOICE->DisposeAll(); }
	//特定のサウンドを再生
	void			PlayVOICE(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
		m_VOICE->Play(ID_t, type_t, TopPositionFlag);
	}
	//特定のサウンドを停止
	void			StopVOICE(int ID_t) noexcept { m_VOICE->Stop(ID_t); }
	//すべてのサウンドを止める
	void			StopVOICEAll() noexcept { m_VOICE->StopAll(); }
	//ボリュームを反映
	void			SetVolVOICEAll(int vol) noexcept { m_VOICE->SetVolAll(vol); }
};

const SoundSingleton* SingletonBase<SoundSingleton>::m_Singleton = nullptr;
```

BGMは長時間のものが主となりますので、CreateSoundDataTypeなどで別途指定が必要な場合があります。
また、重複再生ができるようにするにはまた別途追加してください。
応用すればフェードアウト、フェードインとかも実現できそうかと思いますので試してみてください。

# つかいかた
Addで事前に使うSEをすべてロードした後ボリュームの設定(サウンドオプションで変更した直後とかにも設定しましょう)を行い、任意のタイミングでPlay、使わなくなったらDisposeとなります。
```cpp:WinMain
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
				 LPSTR lpCmdLine, int nCmdShow )
{
	ChangeWindowMode(TRUE);
	if( DxLib_Init() == -1 )	// ＤＸライブラリ初期化処理
	{
		 return -1;				// エラーが起きたら直ちに終了
	}
	SoundSingleton::Create();//


	SoundSingleton::Instance()->AddSE(0, "test.wav");//IDが0のSEを事前にロード

	SoundSingleton::Instance()->SetVolSEAll(128);//読み込んだSEをすべて音量半分にする

	SoundSingleton::Instance()->PlaySE(0, DX_PLAYTYPE_BACK, true);//IDが0のSEを再生

	WaitKey();//何かキーが押されるまで待機

	SoundSingleton::Instance()->DisposeSE(0);//IDが0のSEだけ削除(メインシーンでだけ使うSEだけ削除したいとき)

	//これまでロードしたすべてのSEを削除
	SoundSingleton::Instance()->DisposeSEAll();
	DxLib_End() ;				// ＤＸライブラリ使用の終了処理

	return 0 ;					// ソフトの終了(終了時SoundSingletonまわりはデストラクタが呼ばれない点に注意)
}
```

# まとめ
Singletonパターンはおんせいいがいでもいろいろな部分で使える仕組みなので覚えておいて損はないかと思います。
これをきっかけに色々調べてみて下されれば幸いです(丸投げ)