//
//	clipsnd_uni.cs
//
//	アプリケーションのサウンド関数
//
//	CLiP - Common Library for P/ECE
//	Copyright (C) 2017 Naoyuki Sawa
//
//	* Mon Mar 27 22:39:32 JST 2017 Naoyuki Sawa
//	- 1st リリース。
//	  まだアプリケーションとライブラリの分離が不完全で、当モジュール内にアプリケーション依存の定数やグローバル変数への参照が残っています。
//	  そのため、現在はまだ、当モジュールを直接アプリケーションに含めて、ビルドしています。
//	  引き続き、アプリケーションとライブラリの分離を行って行きます。(※TODO:)
//	* Thu Mar 30 22:26:51 JST 2017 Naoyuki Sawa
//	- 一旦、当モジュールはライブラリから除外しました。
//	  あまりにもアプリケーション依存の部分が多過ぎて、ライブラリに含めたままでは変更が行い辛い問題が生じたからです。
//	  当面は、当モジュールはアプリケーションに含めて変更を進め、いずれ、アプリケーション依存部分と非依存部分の切り分けが出来たら、あらためて非依存部分だけをライブラリに取り込もうと思います。
//	* Sun Apr 02 22:50:54 JST 2017 Naoyuki Sawa
//	- 再生準備通知を追加しました。
//	  詳細は、cliptaps.cの同日のコメントを参照して下さい。
//	* Sun Apr 09 22:05:18 JST 2017 Naoyuki Sawa
//	- prep_phr()を追加しました。
//	  再生処理はSound_Init()が返したpTapSeq,pTapMxrを使って行えるので当モジュールに特有の再生関数は有りませんが、フレーズ準備はpTapSeq,pTapMxrで行えないので当モジュールに関数を追加しました。
//	* Fri Apr 14 22:51:13 JST 2017 Naoyuki Sawa
//	- TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
//	  変更前は、実装を簡単にするために、アプリケーションが経過フレーム数から逆算した時間でTimMgr_Intr()を呼び出しており、処理落ちが発生するとタイマ管理から呼び出される処理も遅れていました。
//	  描画は(目立たないので)それでも構わないのですが、サウンドは遅れると特にループの繋がり等で途切れが目立つので、サウンドは実時間で処理するように変更する事にしました。
//	  元のWindows版(DxlibHelper.cpp)では、フレーム落ちの処理を行っていたので、処理落ちが発生しても上記の問題はあまり起きなかったのですが、現在の所Unityプログラムではフレーム落ちを実装していないので、今回はサウンドを実時間で処理する方法で回避する事にしました。
//	  ちなみに、P/ECEでは1msタイマから直接TapSeq_Exec()を起動しており、組み込み環境ではタイマ管理の1msチャネルからTapSeq_Exec()を駆動していたので、それらの環境では処理落ちの問題は発生しませんでした。
//	- 今回、周期処理の経過時間の求め方を変更したのは、TAPシーケンサの周期処理(TapSeq_Exec())だけです。
//	  TAPミキサーの周期処理(TapMxr_Exec()0の経過時間の求め方は、変更していません。
//	  音量変化は多少ずれても目立たないので、TAPミキサーの周期処理はこれまで通り、アプリケーションが経過フレーム数から逆算した時間に基づいて実行しています。
//	  TAPミキサーの周期処理の経過時間の求め方については、このままで良いと思います。
//	- TAPミキサーの音量をデバイスに即反映する関数として、Sound_UpdVol()を追加しました。
//	  OnApplicationPause(true)時など、音量を即反映したい場合に、アプリケーションから当関数を直接呼び出せるように、publicにしました。
//
#if UNITY_5_3_OR_NEWER
using System;
using static org.piece_me.libclip;
using static Const;
public static partial class Program {
	//*****************************************************************************
	//	ローカル変数
	//*****************************************************************************
	private static class Sound {
		public static ST_AudioHelper		pAudioHelper;
		public static ST_TapSeq			pTapSeq;
		public static ST_TapMxr			pTapMxr;
		public static ST_TapSeqInfo		pTapSeqInfo;
		public static int[][]			TBL_TapMxrChMask;
//{{2017/04/14追加:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
		public static int			t0;	//前回Sound_TapSeq_TimeProc()が呼ばれた時刻[ms]
//}}2017/04/14追加:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
	}
	//*****************************************************************************
	//	Audioプレイヤーの実装
	//*****************************************************************************
	public class AudioPlayer : IAudioPlayer {
		CriAtomExPlayer		criAtomExPlayer;	//AtomExプレイヤー
		//-----------------------------------------------------------------------------
		public AudioPlayer() {
			//AtomExプレイヤーを作成する。
			criAtomExPlayer = new CriAtomExPlayer(260/*最大パス文字列長*/, 1/*同時再生ファイル数*/);
		#if     (!UNITY_EDITOR && UNITY_ANDROID)
			criAtomExPlayer.SetSoundRendererType(CriAtomEx.SoundRendererType.Native);
		#endif//(!UNITY_EDITOR && UNITY_ANDROID)
		}
		//-----------------------------------------------------------------------------
		void IDisposable.Dispose() {
			//AtomExプレイヤーを削除する。
			criAtomExPlayer.Dispose();
		}
		//-----------------------------------------------------------------------------
		AudioPlayerStatus IAudioPlayer.GetStatus() {
			//プレイヤーの状態を取得する。
			return (AudioPlayerStatus)criAtomExPlayer.GetStatus();
		}
		//-----------------------------------------------------------------------------
		void IAudioPlayer.Prepare(int iPhrNo) {
			//フレーズ番号を検査する。
			if((iPhrNo < 1) || (iPhrNo > PhrNo_Max)) { throw new ApplicationException(); }
			//フレーズのパス名を取得する。
			string path = REG_get_string_l(TBL_RegTbl, RegKey_PhrDef, iPhrNo, RegKey_Path);
			if(path == null) { throw new ApplicationException(); }
			//フレーズのパス名を設定する。
			criAtomExPlayer.SetFile(binder, path);
			//ヘッダ解析を開始する。
			criAtomExPlayer.Prepare();
		}
		//-----------------------------------------------------------------------------
		void IAudioPlayer.Start() {
			//再生準備が完了したら、再生が開始するようにしておく。
			criAtomExPlayer.Resume(CriAtomEx.ResumeMode.AllPlayback);
		}
		//-----------------------------------------------------------------------------
		void IAudioPlayer.Stop() {
			//再生,又は,再生準備を停止する。
			criAtomExPlayer.Stop(true/*リリース時間を無視して音声を即座に停止する*/);
			//プレイヤーの状態が停止中になるまで待つ。
			for(;;) {
				CriAtomExPlayer.Status status = criAtomExPlayer.GetStatus();
				if(status == CriAtomExPlayer.Status.Error) { throw new ApplicationException(); }
				if(status == CriAtomExPlayer.Status.Stop) { break; }
				//Unity版のCRIにはAtomEx_ExecuteMain()に相当する関数が無く、ここで呼び出さなくても構わないようです。(実験より)
			}
		}
		//-----------------------------------------------------------------------------
		void IAudioPlayer.SetVolume(double volume) {
			//ボリュームを設定する。
			criAtomExPlayer.SetVolume((float)volume);
			//再生パラメータを更新する。
			criAtomExPlayer.UpdateAll();	//忘れないで!!
		}
		//-----------------------------------------------------------------------------
		void IAudioPlayer.SetPan(double pan) {
			//※TODO:2Dパンを設定するADX2の関数がわかりません…
		}
	}
	//*****************************************************************************
	//	グローバル関数
	//*****************************************************************************
	//サウンドの初期化処理
	public static void Sound_Init(ST_TimMgr pTimMgr, ST_TapSeqInfo pTapSeqInfo, int[][] TBL_TapMxrChMask, out ST_TapSeq pTapSeq, out ST_TapMxr pTapMxr) {
//{{※TODO:削除しろ。
		//※TODO:最終的にアプリケーションから分離するのでこの検査は出来なくなる予定です。
		if(pTapSeqInfo.nDevCh != (TapDevCh_Max+1)) { throw new ApplicationException(); }
		if(pTapSeqInfo.nLogCh != (TapLogCh_Max+1)) { throw new ApplicationException(); }
		if(pTapSeqInfo.nSeqCh != (TapSeqCh_Max+1)) { throw new ApplicationException(); }
		if(TBL_TapMxrChMask.GetLength(0) != (TapLogCh_Max+1)) { throw new ApplicationException(); }
//}}※TODO:削除しろ。
		//オーディオ管理の初期化を実行する。
#if     (!UNITY_EDITOR && UNITY_ANDROID)
		int nCh = criWareInitializer.atomConfig.androidLowLatencyStandardVoicePoolConfig.streamingVoices;
#else //(!UNITY_EDITOR && UNITY_ANDROID)
		int nCh = criWareInitializer.atomConfig.standardVoicePoolConfig.streamingVoices;
#endif//(!UNITY_EDITOR && UNITY_ANDROID)
		if(nCh < pTapSeqInfo.nLogCh) { throw new ApplicationException(); }
		Sound.pAudioHelper = AudioHelper_New(nCh, delegate(object arg) { return new AudioPlayer(); }, null);
		//TAPシーケンサを初期化する。
//{{2017/04/14追加:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
		Sound.t0 = Environment.TickCount;	//前回Sound_TapSeq_TimeProc()が呼ばれた時刻[ms]の初期値を格納する。
//}}2017/04/14追加:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
		Sound.pTapSeqInfo = pTapSeqInfo;
		pTapSeqInfo.fnPlay		= Sound_fnPlay;		//再生開始通知						┐
		pTapSeqInfo.fnStop		= Sound_fnStop;		//再生停止通知						│
		pTapSeqInfo.fnLoop		= Sound_fnLoop;		//自然ループ通知					│
		pTapSeqInfo.fnEnd		= Sound_fnEnd;		//自然停止通知						│※TODO:渡された初期化情報を直接書き換えるのは望ましくない。何とかしろ。(このままでも動作はするが)
		pTapSeqInfo.fnNext		= Sound_fnNext;		//自然遷移通知						│
		pTapSeqInfo.fnCtrl		= Sound_fnCtrl;		//コントロール通知					│
		pTapSeqInfo.fnPrep		= Sound_fnPrep;		//再生準備通知						│	//{{2017/04/02追加:再生準備通知を追加しました。}}
		pTapSeqInfo.fnGetPhrTime	= Sound_fnGetPhrTime;	//フレーズ時間[ms]を取得する。				│
		pTapSeqInfo.fnGetPhrNext	= Sound_fnGetPhrNext;	//フレーズチェインの次のフレーズ番号を取得する。	┘
		Sound.pTapSeq = TapSeq_New(Sound.pTapSeqInfo);
		pTapSeq = Sound.pTapSeq;	//呼び出し側の再生処理のためにTAPシーケンサを返す。
		TimMgr_AddFunc(stTimMgr, TimMgrCh_10ms/*調整可*/, new ST_TimMgrFunc() {
				fn = Sound_TapSeq_TimeProc,
				uItv = 1/*調整可*/,
			});
		//TAPミキサーを初期化する。
		Sound.TBL_TapMxrChMask = TBL_TapMxrChMask;
		Sound.pTapMxr = TapMxr_New(TapMxrCh_Max+1);
		pTapMxr = Sound.pTapMxr;	//呼び出し側の再生処理のためにTAPミキサーを返す。
		TimMgr_AddFunc(stTimMgr, TimMgrCh_10ms/*調整可*/, new ST_TimMgrFunc() {
				fn = Sound_TapMxr_TimeProc,
				uItv = 5/*調整可*/,
			});
	}
	//-----------------------------------------------------------------------------
	//サウンドの周期処理
	public static void Sound_Exec() {
		//オーディオ管理の周期処理を実行する。
		AudioHelper_Exec(Sound.pAudioHelper);
	}
	//-----------------------------------------------------------------------------
	//サウンドの終了処理
	public static void Sound_Exit() {
		//オーディオ管理の終了処理を実行する。
		AudioHelper_Exit(Sound.pAudioHelper);
	}
	//*****************************************************************************
	//	アプリケーション定義のコールバック関数
	//*****************************************************************************
	//再生開始通知
	private static void Sound_fnPlay(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		Sound_play_phr(iLogCh, pLogCh);
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//再生停止通知
	private static void Sound_fnStop(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		Sound_stop_phr(iLogCh, pLogCh);
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//自然ループ通知
	private static void Sound_fnLoop(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		Sound_play_phr(iLogCh, pLogCh);
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//自然停止通知
	private static void Sound_fnEnd(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		Sound_stop_phr(iLogCh, pLogCh);
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//自然遷移通知
	private static void Sound_fnNext(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		Sound_play_phr(iLogCh, pLogCh);
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//コントロール通知
	private static void Sound_fnCtrl(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		Sound_set_att(iLogCh, pLogCh);
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//{{2017/04/02追加:再生準備通知を追加しました。
	private static void Sound_fnPrep(ST_TapSeq pTapSeq, int iLogCh, ST_TapLogCh pLogCh) {
		//再生準備処理は、論理チャネルがデバイスチャネルを獲得していなくても行う。
		{
			//次のフレーズを取得する。
			int iPhr = Sound_fnGetPhrNext(pTapSeq, pLogCh.iPhr);
			//現在のフレーズが、通常フレーズ(-1),又は,完全フレーズ(-2)ならば…
			if(iPhr < 0) {
				//最後のループでなければ(=無限ループ,又は,ループ回数が残っていたら)…
				if(pLogCh.iLoop != 1) {
					//現在のフレーズの再生準備を行う。
					AudioHelper_Prepare(Sound.pAudioHelper, pLogCh.iPhr);
				}
			//現在のフレーズが、ループフレーズ,又は,中間フレーズならば…
			} else {
				//次のフレーズの再生準備を行う。
				AudioHelper_Prepare(Sound.pAudioHelper, iPhr);
			}
		}
	}
	//}}2017/04/02追加:再生準備通知を追加しました。
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//フレーズ時間[ms]を取得する。
	private static int Sound_fnGetPhrTime(ST_TapSeq pTapSeq, int iPhr) {
		int nTime = REG_get_value_l(
			TBL_RegTbl,
			RegKey_PhrDef,
			iPhr,
			RegKey_Time);
		if(nTime == -1) { throw new ApplicationException(); }
		return nTime;
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	//フレーズチェインの次のフレーズ番号を取得する。
	private static int Sound_fnGetPhrNext(ST_TapSeq pTapSeq, int iPhr) {
		int iNext = REG_get_value_l(
			TBL_RegTbl,
			RegKey_PhrDef,
			iPhr,
			RegKey_Next);
		iNext = (iNext << 8) >> 8;	//iNext[23]を符号拡張する。	忘れないで!!
		return iNext;
	}
	//*****************************************************************************
	//	アプリケーション定義のタイマ関数
	//*****************************************************************************
	private static void Sound_TapSeq_TimeProc(object arg) {
		//TAPシーケンサを実行する。
//{{2017/04/14変更:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
//		TapSeq_Exec(Sound.pTapSeq, (10*1)/*調整可*/);
//↓2017/04/14変更:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
		int t1 = Environment.TickCount;	//今回Sound_TapSeq_TimeProc()が呼ばれた時刻[ms]を取得する。
		int dt = t1 - Sound.t0;		//前回Sound_TapSeq_TimeProc()が呼ばれた時刻[ms]からの経過時間を求める。
		Sound.t0 = t1;			//前回Sound_TapSeq_TimeProc()が呼ばれた時刻[ms]を更新する。
		TapSeq_Exec(Sound.pTapSeq, dt);	//TAPシーケンサの周期処理を実行する。
//}}2017/04/14変更:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
	}
	//-----------------------------------------------------------------------------
	private static void Sound_TapMxr_TimeProc(object arg) {
//{{2017/04/14変更:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
//		int iDevCh, iLogCh;
//		//TAPミキサーを実行する。
//		TapMxr_Exec(Sound.pTapMxr, (10*5)/*調整可*/);
//		//各デバイスチャネルについて…
//		for(iDevCh = 0; iDevCh < Sound.pTapSeqInfo.nDevCh; iDevCh++) {
//			//このデバイスチャネルを制御している論理チャネルが有れば…
//			iLogCh = TapSeq_GetDevCh(Sound.pTapSeq, iDevCh).iLogCh;
//			if(iLogCh != byte.MaxValue) {
//				//このデバイスチャネルを制御している論理チャネルを取得する。
//				ST_TapLogCh pLogCh = TapSeq_GetLogCh(Sound.pTapSeq, iLogCh);
//				//この論理チャネルに適用するミキサーチャネルの合計減衰量を、デバイスチャネルに設定する。
//				Sound_set_att(iLogCh, pLogCh);
//			}
//		}
//↓2017/04/14変更:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
		//TAPミキサーを実行する。
		TapMxr_Exec(Sound.pTapMxr, (10*5)/*調整可*/);
		//TAPミキサーをデバイスに反映する。
		Sound_UpdVol();
//}}2017/04/14変更:TAPシーケンサの周期処理は、処理落ちに影響されない実時間で行うように変更しました。
	}
	//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//{{2017/04/14追加:TAPミキサーの音量をデバイスに即反映する関数として、Sound_UpdVol()を追加しました。
	//TAPミキサーをデバイスに反映する。
	// - OnApplicationPause(true)時など、音量を即反映したい場合に、アプリケーションから当関数を直接呼び出せるように、publicにしました。
	public static void Sound_UpdVol() {
		int iDevCh, iLogCh;
		//各デバイスチャネルについて…
		for(iDevCh = 0; iDevCh < Sound.pTapSeqInfo.nDevCh; iDevCh++) {
			//このデバイスチャネルを制御している論理チャネルが有れば…
			iLogCh = TapSeq_GetDevCh(Sound.pTapSeq, iDevCh).iLogCh;
			if(iLogCh != byte.MaxValue) {
				//このデバイスチャネルを制御している論理チャネルを取得する。
				ST_TapLogCh pLogCh = TapSeq_GetLogCh(Sound.pTapSeq, iLogCh);
				//この論理チャネルに適用するミキサーチャネルの合計減衰量を、デバイスチャネルに設定する。
				Sound_set_att(iLogCh, pLogCh);
			}
		}
	}
//}}2017/04/14追加:TAPミキサーの音量をデバイスに即反映する関数として、Sound_UpdVol()を追加しました。
	//*****************************************************************************
	//	ローカル関数
	//*****************************************************************************
	//指定された論理チャネルがデバイスチャネルを獲得していたら、そのデバイスチャネルでフレーズを'非ループ'再生する。
	private static void Sound_play_phr(int iLogCh, ST_TapLogCh pLogCh) {
		int iDevCh = pLogCh.iDevCh;
		//指定された論理チャネルがデバイスチャネルを獲得していたら…
		if(iDevCh != byte.MaxValue) {
			//デバイスチャネルを再生する。
			AudioHelper_Play(Sound.pAudioHelper, iDevCh, pLogCh.iPhr);
			//指定された論理チャネルに適用する合計減衰量を、デバイスチャネルに設定する。
			Sound_set_att(iLogCh, pLogCh);
		}
	}
	//-----------------------------------------------------------------------------
	//指定された論理チャネルがデバイスチャネルを獲得しており,かつ,そのデバイスチャネルで再生中のフレーズ番号が有れば、そのフレーズを停止する。
	private static void Sound_stop_phr(int iLogCh, ST_TapLogCh pLogCh) {
		int iDevCh = pLogCh.iDevCh;
		//指定された論理チャネルがデバイスチャネルを獲得していたら…
		if(iDevCh != byte.MaxValue) {
			//デバイスチャネルを停止する。
			// - 既に停止している場合はダミー処理となり安全です。
			AudioHelper_Stop(Sound.pAudioHelper, iDevCh);
		}
	}
	//-----------------------------------------------------------------------------
	//指定された論理チャネルがデバイスチャネルを獲得していたら、この論理チャネルに適用するミキサーチャネルの合計減衰量を、デバイスチャネルに設定する。
	private static void Sound_set_att(int iLogCh, ST_TapLogCh pLogCh) {
		int iDevCh = pLogCh.iDevCh;
		//指定された論理チャネルがデバイスチャネルを獲得していたら…
		if(iDevCh != byte.MaxValue) {
			//指定された論理チャネルに適用する合計減衰量を取得し、減衰量⇒音量に変換する。
			int iVol = 255;								//音量の最大値
			iVol -= TapMxr_GetAtt(Sound.pTapMxr, Sound.TBL_TapMxrChMask[iLogCh]);	//TAPミキサーの合計減衰量…1次ボリューム相当
			iVol -= pLogCh.iAtt;							//論理チャネルの減衰量   …2次ボリューム相当
			if(iVol < 0) { iVol = 0; }
			//変換した音量を、デバイスチャネルに設定する。
			AudioHelper_SetVolume(Sound.pAudioHelper, iDevCh, iVol / 255.0);
		}
	}
	//*****************************************************************************
	//	再生関数
	//*****************************************************************************
	//再生処理は、Sound_Init()が返したpTapSeq,pTapMxrを使って行って下さい。
	//従って、当モジュールに特有の再生関数は有りません。
	//-----------------------------------------------------------------------------
	//フレーズ準備
	public static bool prep_phr(int iPhrNo) {
		return AudioHelper_Prepare(Sound.pAudioHelper, iPhrNo);
	}
}
#endif//UNITY_5_3_OR_NEWER
