module y4d_sound.soundloader;

private import ytl.y4d_result;
private import y4d_sound.sound;
private import y4d_aux.cacheloader;

///	Sound の一括読み込みクラス。
/**
	Sound を cache したり、自動解放したりいろいろします。
	詳しいことは、親クラス CacheLoader を見てください。

	cacheの単位はbyte。ディフォルトでは64MB。

	oggで1MBモノラルのサウンドは、内部的には(ステレオ44kHzで扱うので)
	10M程度だと考えるべき。<BR>

	例) 5分の16bitステレオ44kHzのBGMならば、
		5分×60秒× 2(ステレオ) × 2(16bits=2byte) × 44kHz
		≒ 54MB <BR>

	これが許せないなら、SoundConfigでサウンドの品質を落とすか、
	ここで確保するバッファをもっと大量に設定するかすべき。<BR>

<PRE>
	int	loadDefFile(char[] filename);
	で読み込む定義ファイルは、
			a.wav , 2 , -1
			b.wav ,,-1
	のように書き、ひとつ目は、ファイル名、二つ目は、読み込む番号、3つ目は
	optionである。optionとして再生するチャンクナンバーを指定できる。
	これは指定しない場合0を意味し、その場合、その時点で再生していないチャンクを
	用いて再生する。

クロスフェードのサンプル：

int main(){
	SoundLoader ca = new SoundLoader;
	ca.loadDefFile("filelist.txt");
	//	この↑ファイルの中身は、
	//		1.ogg
	//		2.ogg
	//	と、再生するファイル名が書いてある

	ca.playBGMFade(0,3000);
	//	0番目のファイルを読み込み、そいつをフェードインさせながら再生
	// ca.get(0).playFade(3000);でも可

	Timer t = new Timer;

	while(t.get()<5000) {}

	// ca.playBGMFade(1,3000);
	//	1番目のファイルをフェードイン再生
	// ca.stopBGMFade(0,3000);
	//	0番目のファイルをフェードアウト再生

	ca.playBGMCrossFade(1,3000); // こんな関数もある。

	t.reset();
	while(t.get()<5000) {}

	return 0;
}

※　サウンドにおけるクロスフェードとは、片方のBGMがフェードしながら、もう片方がフェードインしてくることを言います。

</PRE>

	このoptionは、 Sound クラスで load のときに指定する、
	chunk番号として渡される。指定しなければ0である。

*/
class SoundLoader : public CacheLoader {

	///	指定された番号のオブジェクトを構築して返す
	/**
		暗黙のうちにオブジェクトは構築され、loadされる。

		定義ファイル上に存在しない番号を指定した場合はnullが返る
		ファイルの読み込みに失敗した場合は、nullは返らない。
		定義ファイル上に存在しない番号のものを再生することは
		考慮しなくてもいいと思われる．．。
	*/
	Sound get(int no) {
		return cast(Sound)getHelpper(no);
	}

	///	指定された番号のオブジェクトを構築して返す
	/**
		暗黙のうちにオブジェクトは構築されるが、loadはされない。
		(この点、 get とは異なる)
	*/
	Sound getObj(int no) {
		return cast(Sound) getHelpperObj(no);
	}

	///	Soundのcloneメソッド。
	Sound createInstance() { return new Sound; }

	///	直接指定された番号のサウンドを再生させる
	/**
		失敗時は非0が返る。
		(定義ファイル上に存在しない番号を指定した場合や
		ファイルの読み込みに失敗した場合も非0が返る)
	*/
	y4d_result play(int no) {
		if (!isEnableSound()) return y4d_result.no_error; // そのまま帰ればok
		Sound s = get(no);
		if (s) return s.play();
		return y4d_result.precondition_error; // 存在しない
	}

	///	FadeInつきのplay。FadeInのスピードは[ms]単位。
	y4d_result playFade(int no,int speed){
		if (!isEnableSound()) return y4d_result.no_error; // そのまま帰ればok
		
		Sound s = get(no);

		if (s) return s.playFade(speed);
		return y4d_result.precondition_error; // 存在しない
	}

	/// 直接指定された番号のサウンドを停止させる
	/**
		失敗時は非0が返る。
		(定義ファイル上に存在しない番号を指定した場合や
		ファイルの読み込みに失敗した場合は非0が返る)
	*/
	y4d_result stop(int no) {
		if (!isEnableSound()) return y4d_result.no_error; // そのまま帰ればok
		Sound s = get(no);
		if (s) return s.stop();
		return y4d_result.precondition_error; // 存在しない?
	}

	///	FadeOutつきのstop。FadeOutのスピードは[ms]単位。
	y4d_result stopFade(int no,int speed) {
		if (!isEnableSound()) return y4d_result.no_error; // そのまま帰ればok
		Sound s = get(no);
		if (s) return s.stopFade(speed);
		return y4d_result.precondition_error; // 存在しない?
	}

	//---- 以下は、SE再生関連

	///	SE再生のguard時間の設定
	/**
		SE再生後、一定のフレーム数が経過するまでは、
		そのSEの再生はplaySEを呼び出しても行なわない。
		(ディフォルトでは5フレーム。)

		updateSE()を呼び出したときに1フレーム経過したものとする。
	*/
	void	setGuardTime(int t) { guardTime = t; }

	///	SE再生のguard時間の取得
	int getGuardTime() { return guardTime; }

	/// SE再生のためのもの。毎フレーム1回呼び出すこと！
	/**
		SE再生後、一定のフレーム数が経過するまでは、
		そのSEの再生はplaySEを呼び出しても行なわない。
		これをguard timeと呼ぶ。これは、 setGuardTime で設定する。
		また、毎フレーム updateSE を呼び出す。
		(これを呼び出さないとフレームが経過したとみなされない)
	*/
	void updateSE() {
		foreach(Info info; filelist) {
			SoundInfo s = cast(SoundInfo)info;
			if (s.guardTime > 0) --s.guardTime;
		}
	}

	///	SE再生用のplay関数
	/**
		SEの再生は例えばシューティングで発弾時に行なわれたりして、
		1フレーム間に何十回と同じSE再生を呼び出してしまうことがある。
		そのときに、毎回SEの再生をサウンドミキサに要求していたのでは
		確実にコマ落ちしてしまう。そこで、一定時間は同じ番号のSE再生要求は
		無視する必要がある。それが、このplaySEである。<BR>

		playSEで再生させたものをstopさせるときは
		stop/stopFade関数を用いれば良い。

		この関数を用いる場合は、 updateSE を毎フレーム呼び出さないと
		いけないことに注意。
	*/
	y4d_result playSE(int no){
		if (!isEnableSound()) return y4d_result.no_error; // そのまま帰ればok
		SoundInfo s = cast(SoundInfo)filelist.get(no);
		if (!s) return y4d_result.file_read_error; // 読み込めナーだ
		if (s.guardTime) return y4d_result.no_error;
		// ガード時間中なので再生できナーだった(エラー扱いではない)
		s.guardTime = guardTime;
		return get(no).play();
	}

	///	FadeInつきのplaySE。FadeInのスピードは[ms]単位。
	y4d_result playSEFade(int no,int speed){
		if (!isEnableSound()) return y4d_result.no_error; // そのまま帰ればok
		SoundInfo s = cast(SoundInfo)filelist.get(no);
		if (!s) return y4d_result.file_read_error; // 読み込めナーだ
		if (s.guardTime) return y4d_result.no_error;
		// ガード時間中なので再生できナーだった(エラー扱いではない)
		s.guardTime = guardTime;
		return (cast(Sound)s.obj).playFade(speed);
	}

	///	guardタイムのリセット
	/**
		連続してクリックしたときにクリック音を鳴らさないと
		いけないことがある。SE連続再生を禁止したくないシーンにおいて、
		このメソッドを毎フレームごとor1回呼び出すことにより、
		すべてのサウンドのguardtimeをリセットすることができる
	*/
	void resetGuardTime() {
		foreach(Info info;filelist){
			SoundInfo s = cast(SoundInfo)info;
			if (s) s.guardTime = 0;
		}
	}


	//---- 以下は、BGM再生関連

	///	BGMの再生用play関数
	/**
		メニュー等で、サウンド再生とSEの再生のon/offを切り替えたとする。
		この切り替え自体はenableSound関数を使えば簡単に実現できるが、
		そのときに、(SEはともかく)BGMは鳴らないとおかしい、と言われることが
		ある。そのときにBGMを鳴らすためには、再生要求のあったBGMに関しては
		保持しておき、それを再生してやる必要がある(と考えられる)

		よって、BGM再生に特化した関数が必要なのである。
	*/
	y4d_result playBGM(int no){
		bgmNo = no; return play(no);
	}

	/// FadeInつきの playBGM 。FadeInのスピードは[ms]単位。
	y4d_result playBGMFade(int no,int speed){
		bgmNo = no; return playFade(no,speed);
	}

	/// BGMの再生用のstop関数
	y4d_result stopBGM(){
		if (bgmNo==int.min) {
			return y4d_result.no_error; // 再生中じゃなさげ
		} else {
			int no = bgmNo;
			bgmNo=int.min;
			return stop(no);
		}
	}

	/// FadeOutつきの stopBGM 。FadeInのスピードは[ms]単位。
	y4d_result stopBGMFade(int speed){
		if (bgmNo==int.min) {
			return y4d_result.no_error; // 再生中じゃなさげ
		} else {
			int no = bgmNo;
			bgmNo=int.min;
			return stopFade(no,speed);
		}
	}
	
	/// BGM番号取得 added kow@suhito
	/**
		再生中ならばその番号
		再生していなければ int.min が返る
	*/
	int getBGMNo() {
		return this.bgmNo;
	}
	

	/// BGMのクロスフェード用関数
	/**
		現在再生中のBGMをフェードアウトさせつつ、新しいBGMをフェードインして
		再生するための関数。fade in/outは同じスピードで行なわれる。
		スピードは[ms]単位。fade inするBGMとfade outするBGMの再生chunkが
		異なることが前提。
	*/
	y4d_result playBGMCrossFade(int no,int speed){
		stopBGMFade(speed);
		return playBGMFade(no,speed);
	}

	///	サウンドが再生中かを調べる関数
	/**
		ただし、再生中か調べるために、ファイルを読み込まれていなければ
		ファイルを読み込むので注意。
	*/
	bool isPlay(int no){
		if (!isEnableSound()) return false; // そのまま帰ればok
		Sound s = cast(Sound)get(no);
		return s.isPlay();
	}

	///	サウンドの有効/無効の切り替え
	/**
		初期状態ではenable(有効)状態。
		これを無効にして、そのあと有効に戻したとき、playBGMで再生指定が
		されているBGMがあれば、有効に戻した瞬間にその番号のBGMを
		再生する。<BR>

		無効状態においては、play/playFade/playSE/playBGMの呼び出しすべてに
		おいて何も再生されないことが保証される。(ただし、このクラスを通じて
		再生するときのみの話で、Soundクラスを直接getで取得して再生するならば
		この限りではない)

		また、有効状態から無効状態へ切り替えた瞬間、musicとすべてのchunkでの
		再生を停止させる。
	*/
	void enableSound(bool bEnable){
		if (bEnable == bSoundEnable) return ; // 同じならば変更する必要なし
		if (bEnable) {
			bSoundEnable = true;
			// 状態を先に変更しておかないと、playBGMが無効になったままだ

			//	無効状態から有効化されたならば、
			//	BGM再生要求がヒストリーにあればそれを再生する
			if (bgmNo!=int.min){
				playBGM(bgmNo);
			}
		} else {
			//	無効化するならサウンドすべて停止させなくては..
			if (bgmNo!=int.min){
			//	再生中のものを停止させていることを確認して
			//	再生中のBGMでなければ、BGMは再生されてないと把握し
			//	次にenableSound(true)とされたときも再開させる必要はない
				if (!isPlay(bgmNo)) { 
					bgmNo = int.min;
				} else {
					// 再生中
					int originalBgmNo = bgmNo;
					// 今鳴っているBGMをとめる
					stopBGM();
					// 実際にここでサウンド無効
					bSoundEnable = false;
					// サウンドが無効の状態で、今再生したBGMを設定しておく
					// これによって、次、ONにされたとき、今再生されたいたBGMを復元できる
					playBGM(originalBgmNo);
				}
			}
			
//			Sound.stopAll();
			bSoundEnable = false;
			
			//	このあとで無効化しなくては、bSoundEnableがfalseの状態だと
			//	stop等が利かない可能性がある
		}
	}

	///	enableSoundで設定した値を読み出す
	bool	isEnableSound() { return bSoundEnable; }

	///	foreachに対応させるためのもの
	/**
		事前にすべてのオブジェクトに対して
		setVolumeのようなことをしたいならば、
		foreachがあったほうが便利である。<BR>

		getObjしたものを返すバージョン
	*/
	int opApply(int delegate(inout Sound) dg)
	{
		int result = 0;
		foreach(int key;filelist.getKeys())
		{
			Sound t = getObj(key);
			result = dg(t);
			if (result)	break;
		}
		return result;
	}

	///	foreachに対応させるためのもの
	/**
		事前にすべてのを読み込んでおきたいならば、
		foreachがあったほうが便利である。<BR>

		keyを返すバージョン。
	*/
	int opApply(int delegate(inout int) dg)
	{ return super.opApply(dg); }

	this() {
		guardTime = 5; bgmNo = int.min; bSoundEnable = true;
		cacheSize = 64*1024*1024; // 64MBのsound cache
	}

protected:

	///	サウンドを読み込むための関数。LoadCacheからのオーバーロード
	y4d_result loadFile(char[] filename,inout Object obj,int option1,
		int option2) {
			
			printf("load sound %*s, %d\n", filename, option1);
		if (obj) {
			Sound s = cast(Sound)obj;
			return s.load(filename,option1);
		} else {
			Sound s = new Sound;
			y4d_result result = s.load(filename,option1);
			printf("loader load\n");
			obj = s;
			return result;
		}
	}

	///	サウンドを解放するための関数。LoadCacheからのオーバーロード
	y4d_result releaseFile(inout Object obj) {
		if (obj) {
			Sound s = cast(Sound)obj;
			s.release();
			//	Sound.releaseは失敗しないメソッドなのだ
		} else {
			//	読み込んでないから解放しないでいいんよね？
		}
		return y4d_result.no_error;
	}

	///	解放していいかをチェックして返す
	/***
		BGMとしてエンドレス再生している場合もある。
		サウンドの場合のキャッシュは、最後にアクセスされたものだとしても
		再生中の場合は、解放してはいけない。
	*/
	bool releaseCheck(Info info){
		Sound s = cast(Sound)info.obj;
		if (!s) return true;	//	解放してok
		return cast(bool) !s.isPlay();		//	再生中ならば解放してはいけない
	}

	///	このクラスで必要となるものを構造体に追加
	static class SoundInfo : Info {
		///	SE再生時に用いるguardタイム。これが0のときだけ再生して良い
		int guardTime;
	}

	///	Infoのfactoryをオーバーライド
	override Info createInfo() { return new SoundInfo; }

	/// SEのguard時間(default:5)
	int guardTime;

	///	現在再生しているBGMのナンバー(int.minならば再生してない)
	int bgmNo;

	///	サウンド再生は有効か？(default:true)
	bool bSoundEnable;

}
