#include "stdafx.h"
#include "refCSoundDeviceWASAPI.h"
#include "refCSound.h"

namespace FDK
{
	CSoundDeviceWASAPI::CSoundDeviceWASAPI( EWASAPIデバイスモード mode, Int64 n希望バッファサイズms, Int64 n更新間隔ms, bool b排他モードでの作成失敗時に共有モードでの作成を行う )
	{
		// 初期化。

		this->Disposed = gcnew CDisposed();
		this->bDisposedOutput.Set( false );
		this->hMixer = 0xFFFFFFFF;
		this->nミキサーの1秒あたりのバイト数 = 0;
		this->n累積転送バイト数 = 0;
		this->b最初の実出力遅延算出 = true;

		this->_e出力デバイス = ESoundDeviceType::Unknown;
		this->_n実出力遅延ms = 0;
		this->_n経過時間ms = 0;
		this->_n経過時間を更新したシステム時刻ms = CTimer::n未使用;
		this->_timer = gcnew CTimer( CTimer::E種別::MultiMedia );
		this->b最初の実出力遅延算出 = true;


		// BASS のバージョンチェック。

		int nBASSVersion = ::BASS_GetVersion() >> 16;
		if( nBASSVersion != BASSVERSION )
			throw gcnew DllNotFoundException( String::Format( "bass.dll のバージョンが異なります({0})。このプログラムはバージョン{1}で動作します。", nBASSVersion, BASSVERSION ) );

		int nBASSMixVersion = ::BASS_Mixer_GetVersion() >> 16;
		if( nBASSMixVersion != BASSVERSION )	//  bass と bassmix は必ず同じバージョンでなければならない。
			throw gcnew DllNotFoundException( String::Format( "bassmix.dll のバージョンが異なります({0})。このプログラムはバージョン{1}で動作します。", nBASSMixVersion, BASSVERSION ) );


		// BASS の設定。
				
		bool bRet = ::BASS_SetConfig( BASS_CONFIG_UPDATEPERIOD, 0 ) ? true : false;		// 0:BASSストリームの自動更新を行わない。（BASSWASAPIから行うため）
		Debug::Assert( bRet, String::Format( "BASS_SetConfig() に失敗しました。[{0}", ::BASS_ErrorGetCode() ) );


		// BASS の初期化。

		int nデバイス = 0;		// 0:"no device" … BASS からはデバイスへアクセスさせない。アクセスは BASSWASAPI アドオンから行う。
		DWORD n周波数 = 44100;	// 仮決め。lデバイス（≠ドライバ）がネイティブに対応している周波数であれば何でもいい？ようだ。BASSWASAPIでデバイスの周波数は変えられる。いずれにしろBASSMXで自動的にリサンプリングされる。
		if( !::BASS_Init( nデバイス, n周波数, BASS_DEVICE_CPSPEAKERS, NULL, NULL ) )
			throw gcnew Exception( String::Format( "BASS (WASAPI) の初期化に失敗しました。[{0}]", ::BASS_ErrorGetCode() ) );


		// BASS WASAPI の初期化。

		nデバイス = -1;			// -1:デフォルト出力デバイス
		n周波数 = 0;			// デフォルトデバイスの周波数 (0:"mix device")
		int nチャンネル数 = -1;	// デフォルトデバイスのチャンネル数

		this->dgWASAPIProc = gcnew WASAPIProcDelegate( this, &CSoundDeviceWASAPI::tWASAPI処理 );
		this->gchWASAPIProc = GCHandle::Alloc( this->dgWASAPIProc );
		WASAPIPROC *wasapiProc = reinterpret_cast<WASAPIPROC*>( Marshal::GetFunctionPointerForDelegate( this->dgWASAPIProc ).ToPointer() );
Retry:
		DWORD flags = ( mode == EWASAPIデバイスモード::排他 ) ? ( BASS_WASAPI_AUTOFORMAT | BASS_WASAPI_EXCLUSIVE ) : ( BASS_WASAPI_AUTOFORMAT );
		if( ::BASS_WASAPI_Init( nデバイス, n周波数, nチャンネル数, flags, ( n希望バッファサイズms / 1000.0f ), ( n更新間隔ms / 1000.0f ), wasapiProc, NULL ) )
		{
			if( mode == EWASAPIデバイスモード::排他 )
			{
				#pragma region [ 排他モードで作成成功。]
				//-----------------
				this->e出力デバイス = ESoundDeviceType::ExclusiveWASAPI;

				BASS_WASAPI_INFO wasapiInfo;
				::BASS_WASAPI_GetInfo( &wasapiInfo );
				int n1サンプルのバイト数 = 2 * wasapiInfo.chans;	// default;
				switch( wasapiInfo.format )		// BASS WASAPI で扱うサンプルはすべて 32bit float で固定されているが、デバイスはそうとは限らない。
				{
					case BASS_WASAPI_FORMAT_8BIT: n1サンプルのバイト数 = 1 * wasapiInfo.chans; break;
					case BASS_WASAPI_FORMAT_16BIT: n1サンプルのバイト数 = 2 * wasapiInfo.chans; break;
					case BASS_WASAPI_FORMAT_24BIT: n1サンプルのバイト数 = 3 * wasapiInfo.chans; break;
					case BASS_WASAPI_FORMAT_32BIT: n1サンプルのバイト数 = 4 * wasapiInfo.chans; break;
					case BASS_WASAPI_FORMAT_FLOAT: n1サンプルのバイト数 = 4 * wasapiInfo.chans; break;
				}
				int n1秒のバイト数 = n1サンプルのバイト数 * wasapiInfo.freq;
				Int64 n実バッファサイズms = (Int64)( wasapiInfo.buflen * 1000.0f / n1秒のバイト数 );
				this->n実出力遅延ms = 0;	// 初期値はゼロ

				Trace::TraceInformation( "BASS を初期化しました。(WASAPI排他モード, {0}Hz, {1}ch, フォーマット:{2}, バッファ{3}bytes [{4}ms(希望{5}ms)], 更新間隔{6}ms)",
					wasapiInfo.freq,
					wasapiInfo.chans,
					wasapiInfo.format.ToString(),
					wasapiInfo.buflen,
					n実バッファサイズms.ToString(),
					n希望バッファサイズms.ToString(),
					n更新間隔ms.ToString() );
				//-----------------
				#pragma endregion
			}
			else
			{
				#pragma region [ 共有モードで作成成功。]
				//-----------------
				this->e出力デバイス = ESoundDeviceType::SharedWASAPI;
					
				this->n実出力遅延ms = 0;	// 初期値はゼロ
				BASS_WASAPI_DEVICEINFO devInfo;
				::BASS_WASAPI_GetDeviceInfo( ::BASS_WASAPI_GetDevice(), &devInfo );	// 共有モードの場合、更新間隔はデバイスのデフォルト値に固定される。
				Trace::TraceInformation( "BASS を初期化しました。(WASAPI共有モード, {0}ms, 更新間隔{1}ms)", n希望バッファサイズms, devInfo.defperiod * 1000.0f );
				//-----------------
				#pragma endregion
			}
		}
		else if( mode == EWASAPIデバイスモード::排他 && b排他モードでの作成失敗時に共有モードでの作成を行う)
		{
			#pragma region [ 共有モードでリトライ。]
			//-----------------
			mode = EWASAPIデバイスモード::共有;
			goto Retry;
			//-----------------
			#pragma endregion
		}
		else
		{
			#pragma region [ それでも失敗したら例外発生。]
			//-----------------
			::BASS_Free();
			throw gcnew Exception( String::Format( "BASS (WASAPI) の初期化に失敗しました。[{0}]", ::BASS_ErrorGetCode() ) );
			//-----------------
			#pragma endregion
		}


		// WASAPI 出力情報を取得。

		BASS_WASAPI_INFO info;
		::BASS_WASAPI_GetInfo( &info );
		
		this->_nバッファ長bytes = static_cast<int>( info.buflen );
		this->_nサンプリング周波数Hz = static_cast<int>( info.freq );
		this->_nサンプリングバイト数 = static_cast<int>( ( info.format == BASS_WASAPI_FORMAT_FLOAT ) ? 4 : info.format );
		this->_nチャンネル数 = static_cast<int>( info.chans );

		// WASAPI出力と同じフォーマットを持つ BASS ミキサーを作成。

		this->hMixer = ::BASS_Mixer_StreamCreate(
			info.freq,
			info.chans,
			BASS_MIXER_NONSTOP | BASS_SAMPLE_FLOAT | BASS_STREAM_DECODE );	// デコードのみ＝発声しない。WASAPIに出力されるだけ。
		if( this->hMixer == 0 )
			throw gcnew Exception( String::Format( "BASSミキサの作成に失敗しました。[{0}]", ::BASS_ErrorGetCode() ) );


		// BASS ミキサーの1秒あたりのバイト数を算出。

		BASS_CHANNELINFO mixerInfo;
		::BASS_ChannelGetInfo( this->hMixer, &mixerInfo );
		long nミキサーの1サンプルあたりのバイト数 = mixerInfo.chans * 4;	// 4 = sizeof(FLOAT)
		this->nミキサーの1秒あたりのバイト数 = nミキサーの1サンプルあたりのバイト数 * mixerInfo.freq;


		// 出力を開始。

		::BASS_WASAPI_Start();
	}
	CSoundDeviceWASAPI::~CSoundDeviceWASAPI()
	{
		if( this->Disposed->bDispose済みまたは処理中である )
			return;		// 例外は発出しない。

		if( !this->Disposed->t開始を宣言する() )
			return;		// 例外は発出しない。

		this->bDisposedOutput.Set( true );


		// ファイナライザを呼び出して Unmanaged リソースを解放したのち、ファイナライザの呼び出しを抑制する。タイマの解放より先に呼び出すこと。
		this->!CSoundDeviceWASAPI();


		SAFE_REF_DELETE( this->_timer );		// タイマの解放

		if( this->gchWASAPIProc.IsAllocated )
			this->gchWASAPIProc.Free();

		SAFE_REF_DELETE( this->dgWASAPIProc );

		//GC::SuppressFinalize( this );   --> コンパイラが勝手にやる
	}
	CSoundDeviceWASAPI::!CSoundDeviceWASAPI()
	{
		// Unmanaged リソースを解放する。ディスポーザが呼び出されていればファイナライザは呼び出されないようコンパイラが調整する。

		if( this->hMixer != 0xFFFFFFFF )
			::BASS_StreamFree( this->hMixer );

		::BASS_WASAPI_Free();	// システムタイマの解放より先に呼び出すこと。（tWASAPI処理() の中でシステムタイマを参照してるため）
		::BASS_Free();
	}

	CSound^ CSoundDeviceWASAPI::tサウンドを作成する( String^ stgファイル名 )
	{
		this->Disposed->tDispose済みまたは実行中なら例外発生( "CSoundDeviceWASAPI" );

		if( !File::Exists( stgファイル名 ) )
			return nullptr;
		
		CSound^ sound = gcnew CSound();
		sound->tWASAPIサウンドを作成する( stgファイル名, this->hMixer, this->e出力デバイス );
		return sound;
	}
	CSound^ CSoundDeviceWASAPI::tサウンドを作成する( array<byte>^ byArrWAVファイルイメージ )
	{
		this->Disposed->tDispose済みまたは実行中なら例外発生( "CSoundDeviceWASAPI" );

		CSound^ sound = gcnew CSound();
		sound->tWASAPIサウンドを作成する( byArrWAVファイルイメージ, this->hMixer, this->e出力デバイス );
		return sound;
	}
	void CSoundDeviceWASAPI::tサウンドを作成する( String^ stgファイル名, CSound^% sound )
	{
		this->Disposed->tDispose済みまたは実行中なら例外発生( "CSoundDeviceWASAPI" );

		sound->tWASAPIサウンドを作成する( stgファイル名, this->hMixer, this->e出力デバイス );
	}
	void CSoundDeviceWASAPI::tサウンドを作成する( array<byte>^ byArrWAVファイルイメージ, CSound^% sound )
	{
		this->Disposed->tDispose済みまたは実行中なら例外発生( "CSoundDeviceWASAPI" );

		sound->tWASAPIサウンドを作成する( byArrWAVファイルイメージ, this->hMixer, this->e出力デバイス );
	}

	DWORD CSoundDeviceWASAPI::tWASAPI処理( void *buffer, DWORD length, void *user )
	{
		if( this->bDisposedOutput.Get() )
			return 0;		// 相手は Win32 なので例外は発出しない。

		// BASSミキサからの出力データをそのまま WASAPI buffer へ丸投げ。

		DWORD n今回転送したバイト数 = ::BASS_ChannelGetData( this->hMixer, buffer, length );
		if( n今回転送したバイト数 == static_cast<DWORD>( -1 ) ) n今回転送したバイト数 = 0;		// -1: エラー


		// 経過時間を更新。
		// データの転送差分ではなく累積転送バイト数から算出する。

		UInt64 n未再生バイト数 = (UInt64) ::BASS_WASAPI_GetData( NULL, BASS_DATA_AVAILABLE );	// 誤差削減のため、必要となるギリギリ直前に取得する。
		this->n経過時間ms = ( this->n累積転送バイト数 - n未再生バイト数 ) * 1000 / this->nミキサーの1秒あたりのバイト数;
		this->n経過時間を更新したシステム時刻ms = this->tmシステムタイマ->nシステム時刻ms;

		// 実出力遅延を更新。
		// 未再生バイト数の平均値。

		Int64 n今回の遅延ms = n未再生バイト数 * 1000 / this->nミキサーの1秒あたりのバイト数;
		this->n実出力遅延ms = ( this->b最初の実出力遅延算出 ) ? n今回の遅延ms : ( this->n実出力遅延ms + n今回の遅延ms ) / 2;
		this->b最初の実出力遅延算出 = false;


		// 経過時間を更新後に、今回分の累積転送バイト数を反映。
			
		this->n累積転送バイト数 += n今回転送したバイト数;
		return n今回転送したバイト数;
	}
}