/*	
 *	clipcbx.c
 *
 *	P/ECE シリアルMIDI Driver
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2004 Naoyuki Sawa
 *
 *	* Tue May 18 21:36:00 JST 2004 Naoyuki Sawa
 *	- 作成開始。
 */
#include "clip.h"

/****************************************************************************
 *	ローカル関数宣言
 ****************************************************************************/

static int cbx_read(const unsigned char** ptr, int len);
static int cbx_init(const void* data);
static void cbx_tick();

/****************************************************************************
 *	ローカル変数宣言
 ****************************************************************************/

static CBXDRIVER _driver; /* CBXドライバ */

/****************************************************************************
 *	cbx_read
 ****************************************************************************/

static int
cbx_read(const unsigned char** ptr, int len)
{
	int value;
	int n;

	value = 0;
	if(len == 0) {
		/* 可変長 */
		do {
			n = *(*ptr)++;
			value = (value << 7) | (n & 0x7f);
		} while(n & 0x80);
	} else {
		/* 固定長 */
		do {
			n = *(*ptr)++;
			value = (value << 8) | n;
		} while(--len);
	}

	return value;
}

/****************************************************************************
 *	cbx_init
 ****************************************************************************/

static int
cbx_init(const void* data)
{
	CBXDRIVER* driver = &_driver;
	const unsigned char* ptr = data;
	//
	int value;
	int i_track;
	CBXTRACK* track;

	/* まずゼロクリア。 */
	memset(driver, 0, sizeof(CBXDRIVER));

	/*----- ヘッダチャンク -----*/

	/* チャンクタイプを検査します。 */
	if(memcmp(ptr, "MThd", 4) != 0) return -1;
	ptr += 4;

	/* データ長を検査します。 */
	value = cbx_read(&ptr, 4);
	if(value != 6) return -1;

	/* フォーマットを検査します。 */
	value = cbx_read(&ptr, 2);
	if(value < 0 || 1 < value) return -1; /* フォーマット2は未対応 */

	/* トラック数を格納します。 */
	driver->track_count = cbx_read(&ptr, 2);
	if(driver->track_count < 1) return -1;
	if(driver->track_count > CBXTRACKS) driver->track_count = CBXTRACKS; /* サポートする最大トラック数以降は無視 */

	/* 時間単位を格納します。 */
	driver->time_unit = cbx_read(&ptr, 2);
	if(driver->time_unit & 0x8000) return -1; /* 絶対時間指定は未対応 */

	/*----- トラックチャンク -----*/

	for(i_track = 0; i_track < driver->track_count; i_track++) {
		track = &driver->track[i_track];

		/* チャンクタイプを検査します。 */
		if(memcmp(ptr, "MTrk", 4) != 0) return -1;
		ptr += 4;

		/* データ長を取得します。 */
		value = cbx_read(&ptr, 4);

		/* 初期演奏位置を設定します。 */
		track->pos = ptr;
		ptr += value; /* ptrは次のトラックチャンクを指す */

		/* 初期デルタタイムを設定します。 */
		track->delta = cbx_read(&track->pos, 0);
	}

	/*----- 初期値設定 -----*/

	driver->tempo = 60 * 1000 * 1000 / 120; /* 調整可 */

	return 0;
}

/****************************************************************************
 *	cbx_tick
 ****************************************************************************/

static void
cbx_tick()
{
	CBXDRIVER* driver = &_driver;
	//
	long long delta_step;     /* 時間計算はマイクロ秒単位なので、intでは足りない */
	long long usec_per_delta; /* 時間計算はマイクロ秒単位なので、intでは足りない */
	int i_track;
	int i_channel;
	int event;
	int data1;
	int data2;
	CBXTRACK* track;

	/* 経過デルタタイムを求めます。 */
	driver->timer += 1000 * 1000 / CBX_TIMER_RATE;
	usec_per_delta = driver->tempo / driver->time_unit; /* 除算誤差はほとんど気にならないので無視する */
	delta_step = driver->timer / usec_per_delta;
	driver->timer -= delta_step * usec_per_delta;

	for(i_track = 0; i_track < driver->track_count; i_track++) {
		track = &driver->track[i_track];
		if(!track->pos) continue; /* 演奏完了したトラックは飛ばす */

		/* イベント処理。 */
		track->delta -= delta_step;
		while(track->delta <= 0) {
			/* イベントを読み込みます。 */
			event = cbx_read(&track->pos, 1);
			if(event & 0x80) {
				track->running_status = event;
			} else {
				event = track->running_status;
				track->pos--;
			}

			/* チャネルを取得します。 */
			i_channel = event & 0xf;

			/* イベントの種類によって… */
			switch(event >> 4) {
			case 0x8: /* ノートオフ */
			case 0x9: /* ノートオン */
			case 0xa: /* ポリフォニックキープレッシャー */
			case 0xb: /* コントロールチェンジ */
			case 0xe: /* ピッチベンドチェンジ */
				data1 = cbx_read(&track->pos, 1);
				data2 = cbx_read(&track->pos, 1);
				com_send(&event, 1);
				com_send(&data1, 1);
				com_send(&data2, 1);
				break;
			case 0xc: /* プログラムチェンジ */
			case 0xd: /* チャンネルプレッシャー */
				data1 = cbx_read(&track->pos, 1);
				com_send(&event, 1);
				com_send(&data1, 1);
				break;
			case 0xf:
				switch(i_channel) {
				case 0x0: /* システムエクスクルーシブメッセージ(終端検査あり) */
					com_send(&event, 1);
					data1 = cbx_read(&track->pos, 0); /* F0の次のバイトから終端のF7までの長さ。(終端のF7含む) */
					if(!data1) DIE(); /* 少なくともメーカーIDが必要 */
					do {
						data2 = cbx_read(&track->pos, 1);
						com_send(&data2, 1);
					} while(--data1);
					if(data2 != 0xf7) DIE(); /* 終端検査 */
					break;
				case 0x7: /* システムエクスクルーシブメッセージ(終端検査なし) ※この仕様は間違って理解してるかも? */
					com_send(&event, 1);
					data1 = cbx_read(&track->pos, 0); /* F7の次のバイトから終端までの長さ */
					if(!data1) DIE(); /* 少なくともメーカーIDが必要 */
					do {
						data2 = cbx_read(&track->pos, 1);
						com_send(&data2, 1);
					} while(--data1);
					break;
				case 0xf: /* メタイベント */
					data1 = cbx_read(&track->pos, 1);
					data2 = cbx_read(&track->pos, 1);
					switch(data1) {
					case 0x2f: /* エンドオブトラック */
						if(data2 != 0) DIE();
						track->pos = NULL; /* 演奏完了マーク */
						goto NEXT_TRACK;
						break;
					case 0x51: /* セットテンポ */
						if(data2 != 3) DIE();
						driver->tempo = cbx_read(&track->pos, 3);
						break;
					/*{{未対応*/
					case 0x00: /* シーケンス番号 */
					case 0x01: /* テキスト */
					case 0x02: /* 著作権表示 */
					case 0x03: /* シーケンス名/トラック名 */
					case 0x04: /* 楽器名 */
					case 0x05: /* 歌詞 */
					case 0x06: /* マーカ */
					case 0x07: /* キューポイント */
					case 0x20: /* MIDIチャンネルプレフィックス */
					//case 0x2f: /* エンドオブトラック */
					//case 0x51: /* セットテンポ */
					case 0x54: /* SMPTEオフセット */
					case 0x58: /* 拍子 */
					case 0x59: /* 調号 */
					case 0x7f: /* シーケンサ固有のメタイベント */
					default: /* (不明) */
						track->pos += data2;
						break;
					}
					/*}}未対応*/
					break;
				default:
					DIE();
				}
				break;
			default:
				DIE();
			}

			/* 次のデルタタイムを設定します。 */
			track->delta += cbx_read(&track->pos, 0);
		}
NEXT_TRACK:
	}
}

/****************************************************************************
 *	cbx_play
 ****************************************************************************/

int
cbx_play(const void* data)
{
	/* まず確実に停止します。 */
	cbx_stop();

	/* CBXドライバを初期化します。 */
	if(cbx_init(data) != 0) return -1;

	/* シリアル通信ポートを開きます。 */
	com_open(CBX_BAUDRATE, CBX_PARITY, CBX_BYTESIZE, CBX_STOPBITS, 0, 0, NULL, NULL);

	/* 演奏用タイマコールバックを開始します。 */
	pceTimerSetCallback(CBX_TIMER_CH, PCE_TT_PERIODIC, 1000 / CBX_TIMER_RATE, cbx_tick);

	return 0;
}

/****************************************************************************
 *	cbx_stop
 ****************************************************************************/

void
cbx_stop()
{
	CBXDRIVER* driver = &_driver;

	/* 確実にタイマコールバックを停止します。(既に停止していても安全です) */
	pceTimerSetCallback(CBX_TIMER_CH, PCE_TT_NONE, 0, NULL);

	/* 確実にシリアル通信ポートを閉じます。(既に閉じていても安全です) */
	com_close();

	/* ドライバ構造体をゼロクリアします。(driver->track_count=0:停止中をマーク) */
	memset(driver, 0, sizeof(CBXDRIVER));
}

