/*
 *	clipsndm.c
 *
 *	Sound Manager
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2013 Naoyuki Sawa
 *
 *	* Sat Jul 27 21:43:04 JST 2013 Naoyuki Sawa
 *	- 1st リリース。
 *	* Thu Aug 01 00:52:48 JST 2013 Naoyuki Sawa
 *	- 関数の説明を追記しました。
 *	* Thu Sep 12 23:08:12 JST 2013 Naoyuki Sawa
 *	- SndMgr_AttCh()が、チャネルが再生中の場合のみ処理を行うように、修正しました。
 *	- 修正前のコードは、チャネルが停止中であっても、処理を行ってしまっていました。
 *	  発声していないチャネルに対してfnAtt()を実行するという、無駄がありました。(実害は無いが)
 *	* Fri Sep 13 01:48:44 JST 2013 Naoyuki Sawa
 *	- SndMgr_AttMaster()を追加しました。
 */
#include "clip.h"

/****************************************************************************
 *	アプリケーション用関数
 ****************************************************************************/

ST_SndMgr* SndMgr_Init(const ST_SndMgrInfo* pInfo, int MinCh, int MaxCh) {
	ST_SndMgr* pSndMgr;
	/* 初期化情報が無効ならば、エラー終了します。 */
	if(!pInfo || !pInfo->fnPlay || !pInfo->fnStop || !pInfo->fnStat || !pInfo->fnAtt ||
	   ((unsigned)MinCh > UCHAR_MAX) || ((unsigned)MaxCh > UCHAR_MAX) || (MinCh > MaxCh)) { DIE(); }
	/* 構造体のメモリを確保します。 */
	pSndMgr = calloc(sizeof(ST_SndMgr) + sizeof(ST_SndMgrCh) * (MaxCh - MinCh + 1), 1);
	if(!pSndMgr) { DIE(); } /* メモリ不足 */
	/* 初期化情報を格納します。 */
	pSndMgr->pInfo = pInfo;
	pSndMgr->MinCh = MinCh;
	pSndMgr->MaxCh = MaxCh;
	return pSndMgr;
}

/*--------------------------------------------------------------------------*/

void SndMgr_Free(ST_SndMgr* pSndMgr) {
	/* 未初期化ならば何もしません。 */
	if(!pSndMgr) { return; }
	/* 全チャネルを再生停止します。 */
	SndMgr_StopAll(pSndMgr);
	/* 構造体のメモリを開放します。 */
	free(pSndMgr);
}

/*--------------------------------------------------------------------------*/

void SndMgr_Exec(ST_SndMgr* pSndMgr) {
	int iCh;
	ST_SndMgrCh* pCh;
	/* 未初期化ならば、エラー終了します。 */
	if(!pSndMgr) { DIE(); }
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* チャネルポインタを取得します。 */
		pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
		/* 再生中ならば… */
		if(pCh->iLoop) {
			/* 再生継続ならば… */
			if((*pSndMgr->pInfo->fnStat)(iCh)) {
				/* 再生維持重要度を減らします。 */
				if(pCh->uKeep) { pCh->uKeep--; }
				/* 減衰量変化中ならば…
				 * - 再生停止中のチャネルに対しては、減衰量変化の処理を行う必要がありません。
				 *   発声していないチャネルの減衰量を設定しても、いずれにせよ無音だからです。
				 *   再生停止中のチャネルは、次回の再生開始時に、減衰量をリセットされます。
				 *   従って、再生停止中のチャネルの減衰量変化を行っても、意味がありません。
				 */
				if(pCh->attPeriod) {
					/* 現在の減衰量を計算します。 */
					int attNow = pCh->attNew;		/* 最終減衰量としておく */
					if(++pCh->attTime < pCh->attPeriod) {	/* 変化途中ならば… */
						attNow = ((     attNow *                   pCh->attTime ) +
							  (pCh->attOld * (pCh->attPeriod - pCh->attTime))) / pCh->attPeriod;
					} else {				/* 変化完了ならば… */
						pCh->attPeriod = 0;		/* 変化完了をマークする */
					}
					/* 全体の減衰量を加算します。 */
					if((attNow += pSndMgr->attMaster) > 64) { attNow = 64; }
					/* 減衰量を設定します。 */
					(*pSndMgr->pInfo->fnAtt)(iCh, attNow);
				}
			/* 再生完了ならば… */
			} else {
				/* 停止中をマークします。 */
				pCh->iLoop = 0;
			}
		}
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_PlayCh(ST_SndMgr* pSndMgr, int iCh, int iData, int iLoop) {
	ST_SndMgrCh* pCh;
	/* 未初期化、又は、チャネル番号，ループ指定が無効ならば、エラー終了します。 */
	if(!pSndMgr || (iCh < pSndMgr->MinCh) || (iCh > pSndMgr->MaxCh) || !iLoop) { DIE(); }
	/* チャネルポインタを取得します。 */
	pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
	/* 再生開始します。 */
	(*pSndMgr->pInfo->fnPlay)(iCh, iData, iLoop);
	/* 再生情報を格納します。 */
	pCh->iLoop = iLoop;
	pCh->iData = iData;
	pCh->uKeep = USHRT_MAX;
	/* 減衰量をリセットします。 */
	SndMgr_AttCh(pSndMgr, iCh, 0, 0);
}

/*--------------------------------------------------------------------------*/

void SndMgr_PlayData(ST_SndMgr* pSndMgr, int iData, int iLoop) {
	int iCh;
	ST_SndMgrCh* pCh;
	ST_SndMgrCh* pCh1 = NULL; /* 差し替え候補チャネル1。再生データと同じデータを再生中で停止された、又は、既に停止中だったチャネル */
	ST_SndMgrCh* pCh2 = NULL; /* 差し替え候補チャネル2。再生データと違うデータを有限ループ再生中で、再生維持重要度が最低のチャネル */
	ST_SndMgrCh* pCh3 = NULL; /* 差し替え候補チャネル3。再生データと違うデータを無限ループ再生中で、再生維持重要度が最低のチャネル */
	/* 未初期化、又は、ループ指定が無効ならば、エラー終了します。 */
	if(!pSndMgr || !iLoop) { DIE(); }
	/* 再生データと同じデータを再生中のチャネルを全て停止します。 */
	SndMgr_StopData(pSndMgr, iData);
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* チャネルポインタを取得します。 */
		pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
		/* 停止中ならば… */
		if(!pCh->iLoop) {
			/* 差し替え候補チャネル1を更新します。 */
			if(!pCh1) { pCh1 = pCh; }
		/* 有限ループ再生中ならば… */
		} else if(pCh->iLoop > 0) {
			/* 差し替え候補チャネル2を更新します。 */
			if(!pCh2 || (pCh->uKeep < pCh2->uKeep)) { pCh2 = pCh; }
		/* 無限ループ再生中ならば… */
		} else /*if(pCh->iLoop < 0)*/ {
			/* 新しいループ指定が、無限ループならば… */
			if(iLoop < 0) {
				/* 差し替え候補チャネル3を更新します。 */
				if(!pCh3 || (pCh->uKeep < pCh3->uKeep)) { pCh3 = pCh; }
			/* 新しいループ指定が、有限ループならば… */
			} else {
				/* 既存の無限ループを、新しい有限ループに差し替えることはしません。 */
			}
		}
	}
	/* 差し替え候補チャネル1⇒2⇒3の優先順序で、差し替えチャネルを選択します。 */
//{{2013/07/26:gcc33の最適化バグ回避 (P/ECE研究記録 2013/7/30「gcc33 条件演算子の最適化バグ」参照)
#ifndef PIECE
	pCh = pCh1 ?  pCh1  : pCh2 ?  pCh2  : pCh3; /* gcc33の最適化バグ。処理「a?a:b?b:c」に対し、引数「a=0,b=1,c=0」を渡すと、結果が誤って「0」になる。 */
#else //PIECE
	pCh = pCh1 ?/*pCh1*/: pCh2 ?/*pCh2*/: pCh3; /* gcc33の拡張機能を使って「a?:b?:c」と書けば、gcc33の最適化バグを回避でき、結果が正しく「1」になる。 */
#endif//PIECE
//}}2013/07/26:gcc33の最適化バグ回避 (P/ECE研究記録 2013/7/30「gcc33 条件演算子の最適化バグ」参照)
	/* 差し替えチャネルが有れば… */
	if(pCh) {
		/* チャネル番号を逆算します。 */
		iCh = (pCh - pSndMgr->Ch) + pSndMgr->MinCh;
		/* 再生開始します。 */
		SndMgr_PlayCh(pSndMgr, iCh, iData, iLoop);
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_StopCh(ST_SndMgr* pSndMgr, int iCh) {
	ST_SndMgrCh* pCh;
	/* 未初期化、又は、チャネル番号が無効ならば、エラー終了します。 */
	if(!pSndMgr || (iCh < pSndMgr->MinCh) || (iCh > pSndMgr->MaxCh)) { DIE(); }
	/* チャネルポインタを取得します。 */
	pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
	/* 再生中ならば… */
	if(pCh->iLoop) {
		/* 再生停止します。 */
		(*pSndMgr->pInfo->fnStop)(iCh);
		/* 停止中をマークします。 */
		pCh->iLoop = 0;
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_StopData(ST_SndMgr* pSndMgr, int iData) {
	int iCh;
	ST_SndMgrCh* pCh;
	/* 未初期化ならば、エラー終了します。 */
	if(!pSndMgr) { DIE(); }
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* チャネルポインタを取得します。 */
		pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
		/* 再生中、かつ、再生中のデータと停止データが一致したら… */
		if(pCh->iLoop && (pCh->iData == iData)) {
			/* 再生停止します。 */
			SndMgr_StopCh(pSndMgr, iCh);
		}
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_StopAll(ST_SndMgr* pSndMgr) {
	int iCh;
	/* 未初期化ならば、エラー終了します。 */
	if(!pSndMgr) { DIE(); }
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* 再生停止します。 */
		SndMgr_StopCh(pSndMgr, iCh);
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_AttCh(ST_SndMgr* pSndMgr, int iCh, int attNew, int attPeriod) {
	ST_SndMgrCh* pCh;
	/* 未初期化、又は、チャネル番号，減衰量，変化時間が無効ならば、エラー終了します。 */
	if(!pSndMgr || (iCh < pSndMgr->MinCh) || (iCh > pSndMgr->MaxCh) || ((unsigned)attNew > 64) || ((unsigned)attPeriod > USHRT_MAX)) { DIE(); }
	/* チャネルポインタを取得します。 */
	pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
	/* 再生中ならば…
	 * - 再生停止中のチャネルに対しては、減衰量変化の処理を行う必要がありません。
	 *   発声していないチャネルの減衰量を設定しても、いずれにせよ無音だからです。
	 *   再生停止中のチャネルは、次回の再生開始時に、減衰量をリセットされます。
	 *   従って、再生停止中のチャネルの減衰量変化を行っても、意味がありません。
	 */
	if(pCh->iLoop) {
		/* SndMgr_AttCh()が呼ばれた後、初回にSndMgr_Exec()が呼ばれるまでの間の、減衰量を設定します。
		 * - 即変化でなければ、今回の初期減衰量(=前回の最終減衰量)を設定します。
		 * - 即変化ならば、今回の最終減衰量を設定します。
		 */
		int attNow = attPeriod ? pCh->attNew/*前回の最終減衰量*/ : attNew/*今回の最終減衰量*/;
		/* 全体の減衰量を加算します。 */
		if((attNow += pSndMgr->attMaster) > 64) { attNow = 64; }
		/* 減衰量を設定します。 */
		(*pSndMgr->pInfo->fnAtt)(iCh, attNow);
		/* 変化情報を格納します。 */
		pCh->attPeriod = attPeriod;
		pCh->attTime   = 0;
		pCh->attOld    = pCh->attNew; /* 今回の初期減衰量=前回の最終減衰量 */
		pCh->attNew    = attNew;
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_AttData(ST_SndMgr* pSndMgr, int iData, int attNew, int attPeriod) {
	int iCh;
	ST_SndMgrCh* pCh;
	/* 未初期化、又は、対象データ，減衰量，変化時間が無効ならば、エラー終了します。 */
	if(!pSndMgr || ((unsigned)attNew > 64) || ((unsigned)attPeriod > USHRT_MAX)) { DIE(); }
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* チャネルポインタを取得します。 */
		pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
		/* 再生中、かつ、再生中のデータと設定データが一致したら… */
		if(pCh->iLoop && (pCh->iData == iData)) {
			/* 減衰量を設定します。 */
			SndMgr_AttCh(pSndMgr, iCh, attNew, attPeriod);
		}
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_AttAll(ST_SndMgr* pSndMgr, int attNew, int attPeriod) {
	int iCh;
	/* 未初期化ならば、エラー終了します。 */
	if(!pSndMgr) { DIE(); }
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* 減衰量を設定します。 */
		SndMgr_AttCh(pSndMgr, iCh, attNew, attPeriod);
	}
}

/*--------------------------------------------------------------------------*/

void SndMgr_AttMaster(ST_SndMgr* pSndMgr, int attMaster) {
	int iCh;
	ST_SndMgrCh* pCh;
	/* 未初期化、又は、減衰量が無効ならば、エラー終了します。 */
	if(!pSndMgr || ((unsigned)attMaster > 64)) { DIE(); }
	/* 全体の減衰量を格納します。 */
	pSndMgr->attMaster = attMaster;
	/* 全チャネルを走査して… */
	for(iCh = pSndMgr->MinCh; iCh <= pSndMgr->MaxCh; iCh++) {
		/* チャネルポインタを取得します。 */
		pCh = &pSndMgr->Ch[iCh - pSndMgr->MinCh];
		/* 再生中ならば…
		 * - 再生停止中のチャネルに対しては、減衰量変化の処理を行う必要がありません。
		 *   発声していないチャネルの減衰量を設定しても、いずれにせよ無音だからです。
		 *   再生停止中のチャネルは、次回の再生開始時に、減衰量をリセットされます。
		 *   従って、再生停止中のチャネルの減衰量変化を行っても、意味がありません。
		 */
		if(pCh->iLoop) {
			/* 現在の減衰量を計算します。 */
			int attNow = pCh->attNew;	/* 最終減衰量としておく */
			if(pCh->attPeriod) {		/* 減衰量変化中ならば… */
				attNow = ((     attNow *                   pCh->attTime ) +
					  (pCh->attOld * (pCh->attPeriod - pCh->attTime))) / pCh->attPeriod;
			}
			/* 全体の減衰量を加算します。 */
			if((attNow += pSndMgr->attMaster) > 64) { attNow = 64; }
			/* 減衰量を設定します。 */
			(*pSndMgr->pInfo->fnAtt)(iCh, attNow);
		}
	}
}

