//------------------------------------------------------------------------------
//	WaveformAudio クラス
//		ウェーブフォームオーディオを使うための処理をまとめたもの
//		作成者：三上直樹，2013/02/04 作成，©三上直樹 2013
//------------------------------------------------------------------------------

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;	// Marshal，GCHandle 用

namespace Mikami.WaveformAudio
{
	public partial class WaveformAudio
	{
		private byte[] inBuffer_;		 // read() で読み込んだデータの領域
		private byte[] outBuffer_;		 // 出力用データ領域
		private short[] xn_;			 // readBuffer をモノラルの short 型に変換したデータ
		private WAVEFORMATEX fmtIn_;	 // 読み込んだファイルの fmt チャンクに対応
		private WAVEFORMATEX fmtOut_;	 // 書き込み用ファイルの fmt チャンクに対応
		private int nDataByte_;			 // data チャンクのサイズ（バイト単位）
		private int nData_;				 // short型またはfloat型に変換されたデータの数
		private bool flagOpen_;			 // 出力デバイスがオープンの場合に true
		private IntPtr hwo_;			 // Waveform audio 出力デバイスのハンドラ
		private WAVEHDR waveHeader_;	 // 再生用 WAVEHDR 構造体
		private readonly int maxSize_;	 // 読み込むファイルのデータ領域の最大値（MBytes）
		private readonly int sizeOfWAVEHDR_;		// WAVEHDR 構造体のサイズ
		private readonly int sizeOfWAVEFORMATEX_;	// WAVEFORMATEX 構造体のサイズ

		// コンストラクタ（読み込むファイルのデータ領域の最大値： maxSize で設定）
		public WaveformAudio(int maxSize = 64)	// maxSize の単位： MBytes
		{
			flagOpen_ = false;
			maxSize_			= maxSize*1024*1024;
			sizeOfWAVEHDR_		= Marshal.SizeOf(typeof(WAVEHDR));
			sizeOfWAVEFORMATEX_	= Marshal.SizeOf(typeof(WAVEFORMATEX));
		}

		// デストラクタ
		~WaveformAudio() { ResetPlay(); }

		// ウェーブファイルの読み出し
		public bool Read(string fileName)
		{
			// 内部入出力バッファを使用してウェーブファイルをオープンする
			IntPtr hmmio = mmioOpen(fileName, IntPtr.Zero,
									MMIO_READ | MMIO_ALLOCBUF);
			if (hmmio == IntPtr.Zero)
				return ErrorMessage("ファイルのオープンに失敗しました．");

			// RIFFチャンク内でWAVEチャンクを探す
			MMCKINFO parentChunk = new MMCKINFO();	// 親チャンクの情報
			parentChunk.fccType = mmioStringToFOURCC("WAVE", 0);
			uint rc = mmioDescend(hmmio, ref parentChunk, 0, MMIO_FINDRIFF);
			if (rc != MMSYSERR_NOERROR)
				return ErrorMessageClose("ウェーブフォームファイルではありません．",
										 hmmio);

			// fmtチャンクの処理の開始
			if (!FmtChunk(hmmio, parentChunk)) return false;

			// data チャンクの処理
			if (!DataChunk(hmmio, parentChunk)) return false;

			nData_ = nDataByte_/fmtIn_.nBlockAlign;

			return true;		// 正常終了
		}

		// ウェーブファイルへの書き込み
		public bool Write(string fileName)
		{
			if (nData_ == 0) return ErrorMessage("データが準備されていません");
			nDataByte_ = fmtOut_.nBlockAlign*nData_;

			// 内部入出力バッファを使用してウェーブファイルをオープンする
			IntPtr hmmio = mmioOpen(fileName, IntPtr.Zero,
									MMIO_CREATE | MMIO_WRITE | MMIO_ALLOCBUF);
			if(hmmio == IntPtr.Zero)
				return ErrorMessage("ファイルのオープンに失敗しました．");

			// RIFF チャンクを作成
			mmioSeek(hmmio, 0, SEEK_SET);			// ファイルの先頭へシークする
			MMCKINFO riffChunk = new MMCKINFO();	// Riff チャンクの情報
			riffChunk.fccType = mmioStringToFOURCC("WAVE", 0);
			riffChunk.ckSize  = 0;
			mmioCreateChunk(hmmio, ref riffChunk, MMIO_CREATERIFF);

			// fmt チャンクの作成と書き込み
			MMCKINFO subChunk = new MMCKINFO();		// サブチャンクの情報
			subChunk.ckid	= mmioStringToFOURCC("fmt", 0);
			subChunk.ckSize = (uint)sizeOfWAVEFORMATEX_;
			mmioCreateChunk(hmmio, ref subChunk, 0);
			mmioWrite(hmmio, ref fmtOut_, sizeOfWAVEFORMATEX_);

			// fmt チャンクから RIFF チャンクへ移動
			mmioAscend(hmmio, ref subChunk, 0);

			// dataチャンクの作成と書き込み
			subChunk.ckid	= mmioStringToFOURCC("data", 0);
			subChunk.ckSize = (uint)nDataByte_;
			mmioCreateChunk(hmmio, ref subChunk, 0);

			// dataチャンク用のバッファのアドレスを固定して書き込む
			GCHandle handle = GCHandle.Alloc(outBuffer_, GCHandleType.Pinned);
			int rc = mmioWrite(hmmio, handle.AddrOfPinnedObject(), nDataByte_);
			if (handle.IsAllocated) handle.Free();
			if (rc != nDataByte_)
				return ErrorMessageClose("ファイルの書き込みに失敗しました．",
										 hmmio);

			// data チャンクから RIFF チャンクへ戻る
			mmioAscend(hmmio, ref subChunk, 0);
			// RIFF チャンクの外部へ移動
			mmioAscend(hmmio, ref riffChunk, 0);

			mmioFlush(hmmio, 0);
			mmioClose(hmmio, 0);

			return true;		// 正常終了
		}

		// 読み込んだデータを short 型，モノラルに変換すると共に元の WAVEFORMATEX
		// 構造体の内容を 16 ビット，モノラルのデータに対応するように変換
		public void ToShortMono()
		{
			xn_ = new short[nData_];

			if (fmtIn_.wBitsPerSample == 8)			// 8 ビット
			{
				if (NumberOfChannels == 1)			// モノラル
				for (int j=0; j<nData_; j++)
					xn_[j] = (short)((GetByte(j) - 128) << 8);
				else								// ステレオ
				for (int j=0; j<nData_; j++)
					xn_[j] = (short)((((GetByte(2*j) +
										GetByte(2*j+1)) - 256)) << 7);
			}
			else									// 16 ビット
			{
				if (NumberOfChannels == 1)			// モノラル
					for (int j=0; j<nData_; j++) xn_[j] = GetShort(2*j);
				else								// ステレオ
					for (int j=0; j<nData_; j++)
						xn_[j] = (short)((GetShort(4*j) + GetShort(4*j+2))/2);
			}

			fmtOut_.wFormatTag = fmtIn_.wFormatTag;	// 変更なし
			fmtOut_.nChannels = 1;					// 強制的にモノラルにする
			fmtOut_.nSamplesPerSec = fmtIn_.nSamplesPerSec;	// 変更なし
			fmtOut_.nAvgBytesPerSec = 2*fmtIn_.nSamplesPerSec;
			fmtOut_.nBlockAlign = 2;				// 1 サンプル当たり 2 バイト
			fmtOut_.wBitsPerSample = 16;			// 強制的に 16 ビットにする
			fmtOut_.cbSize = fmtIn_.cbSize;			// 変更なし
		}

		// 読み込んだデータをそのままウェーブフォーム出力デバイスから再生
		public bool PlayOriginal() { return PlayAudio(fmtIn_, inBuffer_); }

		// 出力用データ領域のデータをウェーブフォーム出力デバイスから再生
		public bool Play() { return PlayAudio(fmtOut_, outBuffer_); }

		// 再生の終了を待つ
		public void WaitEndOfPlay()
		{
			waveOutUnprepareHeader(hwo_, ref waveHeader_, (uint)sizeOfWAVEHDR_);
			while (true) if (!IsPlay) break;	// デバイスの再生終了まで待つ
			waveOutClose(hwo_);
			flagOpen_ = false;
		}

		// 再生の強制終了
		public void ResetPlay()
		{
			if (!flagOpen_) return;
			waveOutReset(hwo_);
			waveOutUnprepareHeader(hwo_, ref waveHeader_, (uint)sizeOfWAVEHDR_);
			waveOutClose(hwo_);
			flagOpen_ = false;
		}

		// 16 ビットモノラルのデータの取得（型変換なし）
		public int GetData(short[] yn)
		{
			if (xn_.Length == 0)
				ErrorMessage("変換されたデータが準備されていません．");
			else
				for (int n=0; n<xn_.Length; n++) yn[n] = xn_[n];
			return xn_.Length;
		}

		// 16 ビットモノラルのデータの取得（float型に変換）
		public int GetData(float[] yn)
		{
			if (xn_.Length == 0)
				ErrorMessage("変換されたデータが準備されていません．");
			else
				for (int n=0; n<xn_.Length; n++) yn[n] = (float)xn_[n];
			return xn_.Length;
		}

		// short型のデータを16ビットモノラルのデータとして出力用データ領域に設定
		public void SetData(short[] un, int fs)
		{
			SetWAVEFORMATEX_16M(fs);
			nData_ = un.Length;
			outBuffer_ = new byte[sizeof(short)*nData_];

			for (int n = 0; n < nData_; n++)
				Array.Copy(BitConverter.GetBytes(un[n]), 0, outBuffer_, 2*n, 2);
		}

		// float型のデータを16ビットモノラルのデータとして出力用データ領域に設定
		public void SetData(float[] un, int fs)
		{
			SetWAVEFORMATEX_16M(fs);
			nData_ = un.Length;
			outBuffer_ = new byte[sizeof(short)*nData_];

			for (int n=0; n<nData_; n++)
			{
				if ( (un[n] < -32768.0F) || ( 32767.0F < un[n]) )
				{
					ErrorMessage("short型で表せないデータです．");
					return;
				}
				Array.Copy(BitConverter.GetBytes((short)un[n]), 0,
						   outBuffer_, 2*n, 2);
			}
		}

		// プロパティ：再生中かどうかを調べる
		public bool IsPlay
		{
			get
			{
				if (!flagOpen_) return false;
				return (waveHeader_.dwFlags & WHDR_DONE) != WHDR_DONE;
			}
		}
		
		// プロパティ：データの長さの取得
		public int Length {	get { return nData_; } }

		// プロパティ：標本化周波数の取得
		public int SamplingFrq { get { return (int)fmtIn_.nSamplesPerSec; } }

		// プロパティ：入力ファイルのチャンネル数取得
		public int NumberOfChannels { get { return fmtIn_.nChannels; } }

		//======================================================
		//			以下は protected メソッド
		//======================================================

		// read() で読み込んだデータから byte 型データを取り出す
		protected short GetByte(int n) { return (short)inBuffer_[n]; }

		// read() で読み込んだデータから short 型データを取り出す
		protected short GetShort(int n)
		{
			return BitConverter.ToInt16(inBuffer_, n);
		}

		// エラーメッセージとリターンコード：false
		protected bool ErrorMessage(string message)
		{
			MessageBox.Show(message, "*.wav 操作エラー",
							MessageBoxButtons.OK, MessageBoxIcon.Warning);
			return false;
		}

		// エラーメッセージ，ファイルのクローズ，リターンコード：false
		protected bool ErrorMessageClose(string message, IntPtr hmmio)
		{
			mmioClose(hmmio, 0);
			return ErrorMessage(message);
		}

		//======================================================
		//		以下は派生クラス用 protected メソッド，プロパティ
		//		(基本クラスでは使わない)
		//======================================================

		// ステレオ出力用バッファのサイズ設定
		protected void SetOutBufferSize(int n)
		{
			nData_ = n;
			outBuffer_ = new byte[sizeof(short)*nData_*2];
		}

		// 出力用バッファにデータを設定
		protected void SetOutBuffer(short x, int n)
		{
			Array.Copy(BitConverter.GetBytes(x), 0, outBuffer_, 2*n, 2);
		}

		// 入力用  WAVEFORMATEX 型構造体の取得用プロパティ
		protected WAVEFORMATEX FormatIn { get { return fmtIn_; } }

		// 出力用  WAVEFORMATEX 型構造体の設定用プロパティ
		protected WAVEFORMATEX FormatOut { set { fmtOut_ = value; } }

		//======================================================
		//			以下は private メソッド
		//======================================================

		// fmt チャンクの処理
		private bool FmtChunk(IntPtr hmmio, MMCKINFO parentChunk)
		{
			// fmt チャンクを探す
			MMCKINFO subChunk = new MMCKINFO();			// サブチャンクの情報
			subChunk.ckid = mmioStringToFOURCC("fmt", 0);
			uint rc = mmioDescend(hmmio, ref subChunk, ref parentChunk, MMIO_FINDCHUNK);
			if (rc != MMSYSERR_NOERROR)
				return ErrorMessageClose("fmt チャンクが見つかりません．", hmmio);

			// fmtチャンクを読み取る
			int irc = mmioRead(hmmio, ref fmtIn_, (int)subChunk.ckSize);
			if (irc == -1)
				return ErrorMessageClose("fmt チャンク読み取りエラー．", hmmio);

			// fmtチャンクの外部に移動する
			rc = mmioAscend(hmmio, ref subChunk, 0);

			if (fmtIn_.wFormatTag == 1) return true;	// 正常終了

			fmtOut_.wFormatTag = fmtIn_.wFormatTag;
			return ErrorMessageClose("PCM 以外のデータ形式は扱えません．", hmmio);
		}

		// data チャンクの処理
		private bool DataChunk(IntPtr hmmio, MMCKINFO parentChunk)
		{
			// data チャンクを探す
			MMCKINFO subChunk = new MMCKINFO();			// サブチャンクの情報
			subChunk.ckid = mmioStringToFOURCC("data", 0);
			uint rc = mmioDescend(hmmio, ref subChunk, ref parentChunk, MMIO_FINDCHUNK);
			if (rc != MMSYSERR_NOERROR)
				return ErrorMessageClose("data チャンクが見つかりません．", hmmio);

			// dataチャンク用のバッファを割り当てる
			nDataByte_ = (int)subChunk.ckSize;
			if (nDataByte_ > maxSize_)
				return ErrorMessageClose("データ数が最大値（" +
										 (maxSize_/(1024*1024)).ToString() +
										 "MBytes）以上です．", hmmio);

			inBuffer_ = new byte[nDataByte_];

			// dataチャンク用のバッファのアドレスを固定して読み込む
			GCHandle handle = GCHandle.Alloc(inBuffer_, GCHandleType.Pinned);
			int irc = mmioRead(hmmio, handle.AddrOfPinnedObject(), nDataByte_);
			if (handle.IsAllocated) handle.Free();
			if (irc != nDataByte_)
				return ErrorMessageClose("data チャンク読み取りエラー．", hmmio);

			mmioClose(hmmio, 0);

			return true;
		}

		// ウェーブフォーム出力デバイスから音を出力
		private bool PlayAudio(WAVEFORMATEX fmt, byte[] buffer)
		{
			// データが PCM であることを確認
			if (fmt.wFormatTag != 1)
				return ErrorMessage("PCM 以外のデータ形式は扱えません．");

			ResetPlay();

			// ウェーブフォーム出力デバイスのオープン
			// デフォルトの再生でデバイスを使用（WAVE_MAPPER 指定の場合）
			uint rc = waveOutOpen(ref hwo_, WAVE_MAPPER, ref fmt, 0, 0, CALLBACK_NULL);
			if (rc != MMSYSERR_NOERROR)
				return ErrorMessage("ウェーブフォームドライバをオープンできません．");

			flagOpen_ = true;

			// dataチャンク用のバッファのアドレスを固定して出力する
			GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
			waveHeader_.lpData = handle.AddrOfPinnedObject();
			waveHeader_.dwBufferLength = (uint)(fmt.nBlockAlign*nData_);
			waveHeader_.dwFlags = 0;
			waveHeader_.dwLoops = 0;

			rc = waveOutPrepareHeader(hwo_, ref waveHeader_, (uint)sizeOfWAVEHDR_);
			if (handle.IsAllocated) handle.Free();
			if (rc != MMSYSERR_NOERROR)
			{
				ErrorMessage("ウェーブヘッダの初期化に失敗しました．");
				waveOutClose(hwo_);
				return false;
			}

			waveOutWrite(hwo_, ref waveHeader_, (uint)sizeOfWAVEHDR_);

			return true;
		}

		// 出力用 WAVEFORMATEX 型の構造体のメンバを 16 ビットモノラルに設定
		private void SetWAVEFORMATEX_16M(int fs)
		{
			fmtOut_.wFormatTag = 1;					// PCM
			fmtOut_.nChannels = 1;					// モノラル
			fmtOut_.nSamplesPerSec = (uint)fs;		// 標本化周波数（Hz）
			fmtOut_.nAvgBytesPerSec = (uint)(2*fs);	// 転送速度（bytes/s）
			fmtOut_.nBlockAlign = 2;				// 1 サンプル当たり 2 バイト
			fmtOut_.wBitsPerSample = 16;			// 16 ビット
			fmtOut_.cbSize = 0;						// 拡張情報不要
		}
	}
}
