/*	
 *	clipmidi.h
 *
 *	P/ECE MIDI Driver
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2003 Naoyuki Sawa
 *
 *	* Tue Aug 30 22:28:00 JST 2003 Naoyuki Sawa
 *	- 新規作成。
 *	* Tue Sep 02 24:00:00 JST 2003 Naoyuki Sawa
 *	- 音色マップ対応。
 *	- リズムパート対応。
 *	- チャネル内和音対応。
 *	* Tue Sep 03 04:02:00 JST 2003 Naoyuki Sawa
 *	- テンポずれ修正。
 */
#include "clip.h"

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

static int midi_stream_callback(short* wbuff, int param);
static int midi_read(const unsigned char** ptr, int len);
static void midi_note_on(MIDIDRIVER* driver, int i_channel, int note, int velocity);
static void midi_note_off(MIDIDRIVER* driver, int i_channel, int note, int velocity);

/****************************************************************************
 *	ローカル変数定義
 ****************************************************************************/

/* MIDIノートナンバー→MODピリオド値変換表(調整可) */
static const short note_period[128] = {
	13696,12928,12192,11520,10848,10240, 9664, 9120, 8608, 8128, 7680, 7248,	/*   0〜 11 */
	 6848, 6464, 6096, 5760, 5424, 5120, 4832, 4560, 4304, 4064, 3840, 3624,	/*  12〜 23 */
	 3424, 3232, 3048, 2880, 2712, 2560, 2416, 2280, 2152, 2032, 1920, 1812,	/*  24〜 35 */
	 1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016,  960,  906,	/*  36〜 47 */
	  856,  808,  762,  720,  678,  640,  604,  570,  538,  508,  480,  453,	/*  48〜 59 */
	  428,  404,  381,  360,  339,  320,  302,  285,  269,  254,  240,  226,	/*  60〜 71 */
	  214,  202,  190,  180,  170,  160,  151,  143,  135,  127,  120,  113,	/*  72〜 83 */
	  107,  101,   95,   90,   85,   80,   75,   71,   67,   63,   60,   56,	/*  84〜 95 */
	   53,   50,   47,   45,   42,   40,   37,   35,   33,   31,   30,   28,	/*  96〜107 */
	   26,   25,   23,   22,   21,   20,   18,   17,   16,   15,   15,   14,	/* 108〜119 */
	   13,   12,   11,   11,   10,   10,    9,    8,				/* 120〜127 */
};

#define RHYTHM_NOTE	60	/* リズム音色は常にこの音程で鳴らす(調整可) */

/****************************************************************************
 *	midi_play
 ****************************************************************************/

int
midi_play(const void* data, const void* inst, const MIDIMAP* map)
{
	static MIDIDRIVER driver; /* ＳＴＡＴＩＣです！ */

	/* まず確実に停止します。 */
	midi_stop();

	/* MIDIドライバを初期化します。 */
	if(midi_init(&driver, data, inst, map) != 0) return -1;

	/* ストリーム再生を開始します。 */
	stream_play(MODBUFLEN, midi_stream_callback, (int)&driver, 0);

	return 0;
}

/****************************************************************************
 *	midi_stop
 ****************************************************************************/

void
midi_stop()
{
	/* ストリーム再生を停止します。 */
	stream_stop();

	/* MIDIドライバのクリーンアップは不要です。 */
}

/****************************************************************************
 *	midi_init
 ****************************************************************************/

int
midi_init(MIDIDRIVER* driver, const void* data, const void* inst, const MIDIMAP* map)
{
	const unsigned char* ptr = data;
	int value, i_track;
	MIDITRACK* track;

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

	/* 音色マップを格納します。 */
	driver->map = map;

	/*----- MOD音源初期化 -----*/

	if(mod_init(&driver->mod, inst) != 0) DIE();
	driver->mod.chans = MODCHANS; /* 音源として全チャネル使用 */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	return 0;
}

/****************************************************************************
 *	midi_tick
 ****************************************************************************/

int
midi_tick(MIDIDRIVER* driver, short wbuff[/*MODBUFLEN*/])
{
	int retval;

	/* シーケンス処理。 */
	retval = midi_seq(driver);
	if(retval != 0) return retval;

	/* ミキシング処理。 */
	retval = midi_mix(driver, wbuff);
	if(retval != 0) return retval;

	return 0;
}

/****************************************************************************
 *	midi_seq
 ****************************************************************************/

int
midi_seq(MIDIDRIVER* driver)
{
	long long delta_step, usec_per_delta; /* 時間計算はミリ秒単位なので、intでは足りない */
	int playing, i_track, i_channel, event, data1, data2;
	MIDITRACK* track;

	/* 経過デルタタイムを求めます。 */
	driver->timer += 1000 * 1000 / 50/*ストリーム再生の割り込みサイクル*/;
	usec_per_delta = driver->tempo / driver->time_unit; /* 除算誤差はほとんど気にならないので無視する */
	delta_step = driver->timer / usec_per_delta;
	driver->timer -= delta_step * usec_per_delta;

	playing = 0; /* 演奏中のトラック数 */
	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 = midi_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: /* ノートオフ */
				data1 = midi_read(&track->pos, 1);
				data2 = midi_read(&track->pos, 1);
				if(i_channel != 10 - 1) { /* MIDIのノートオフイベントは無視した方が聴こえが良い */
					midi_note_off(driver, i_channel, data1, data2);
				}
				break;
			case 0x9: /* ノートオン */
				data1 = midi_read(&track->pos, 1);
				data2 = midi_read(&track->pos, 1);
				if(data2 != 0) {
					midi_note_on(driver, i_channel, data1, data2);
				} else { /* ベロシティ0のノートオンは、ノートオフの代用 */
					if(i_channel != 10 - 1) { /* MIDIのノートオフイベントは無視した方が聴こえが良い */
						midi_note_off(driver, i_channel, data1, data2);
					}
				}
				break;
			case 0xa: /* ポリフォニックキープレッシャー */
				data1 = midi_read(&track->pos, 1);
				data2 = midi_read(&track->pos, 1);
				/* 未対応 */
				break;
			case 0xb: /* コントロールチェンジ */
				data1 = midi_read(&track->pos, 1);
				data2 = midi_read(&track->pos, 1);
				/* 未対応 */
				break;
			case 0xc: /* プログラムチェンジ */
				data1 = midi_read(&track->pos, 1);
				driver->channel[i_channel].program = data1;
				break;
			case 0xd: /* チャンネルプレッシャー */
				data1 = midi_read(&track->pos, 1);
				/* 未対応 */
				break;
			case 0xe: /* ピッチベンドチェンジ */
				data1 = midi_read(&track->pos, 1);
				data2 = midi_read(&track->pos, 1);
				/* 未対応 */
				break;
			case 0xf:
				switch(i_channel) {
				case 0x0: /* システムエクスクルーシブメッセージ(終端検査あり) */
					data1 = midi_read(&track->pos, 0);
					track->pos += data1 - 1;
					data1 = midi_read(&track->pos, 1);
					if(data1 != 0xf7) DIE();
					break;
				case 0x7: /* システムエクスクルーシブメッセージ(終端検査なし) */
					data1 = midi_read(&track->pos, 0);
					track->pos += data1;
					break;
				case 0xf: /* メタイベント */
					data1 = midi_read(&track->pos, 1);
					data2 = midi_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 = midi_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 += midi_read(&track->pos, 0);
		}
		playing++; /* 演奏中のトラック数をカウントアップ */
NEXT_TRACK:
	}

	/* 演奏中のトラックがあれば0、なければ1を返します。 */
	return playing == 0;
}

/****************************************************************************
 *	midi_mix
 ****************************************************************************/

int
midi_mix(MIDIDRIVER* driver, short wbuff[/*MODBUFLEN*/])
{
	int result, i_voice;
	MIDIVOICE* voice;
	MODCHANNEL* modch;

	/* 発声処理はMOD音源に任せます。 */
	result = mod_mix(&driver->mod, wbuff);

	/* MODチャネル管理情報を更新。 */
	voice = &driver->voice[0];
	modch = &driver->mod.channel[0];
	for(i_voice = 0; i_voice < MODCHANS; i_voice++) {
		/* 発声中マークされていたら... */
		if(voice->age) {
			if(modch->period) {
				/* 発生継続中なら、旧さをカウントアップ。 */
				voice->age++;
			} else {
				/* 発声終了していたら、管理情報もクリア。 */
				voice->age = 0;
			}
		}
		voice++;
		modch++;
	}

	return result;
}

/****************************************************************************
 *	midi_stream_callback
 ****************************************************************************/

static int
midi_stream_callback(short* wbuff, int param)
{
	return midi_tick((MIDIDRIVER*)param, wbuff);
}

/****************************************************************************
 *	midi_read
 ****************************************************************************/

static int
midi_read(const unsigned char** ptr, int len)
{
	int value, 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;
}

/****************************************************************************
 *	midi_note_on
 ****************************************************************************/

static void
midi_note_on(MIDIDRIVER* driver, int i_channel, int note, int velocity)
{
	const MIDIMAP* map;
	MIDICHANNEL* midch;
	MIDIVOICE* voice;
	MODCHANNEL* modch;
	int smpno, i_voice, sel_voice, age, max_age;

	/* 同じチャネル・ノートが既に鳴っていたら、確実に停止します。 */
	midi_note_off(driver, i_channel, note, velocity);

	/* MIDIプログラムナンバーまたはMIDIノートナンバーから、MODサンプルナンバーへ変換します。 */
	map = driver->map;
	if(i_channel != 10 - 1) { /* メロディ */
		midch = &driver->channel[i_channel];
		smpno = map->melody[midch->program];
	} else {		  /* リズム */
		smpno = map->rhythm[note];
	}
	if(!smpno) return; /* 音色マップが設定されていなければ鳴らさない */

	/* 空きチャネル、またはいちばん旧いチャネルを探します。 */
	voice = &driver->voice[0];
	sel_voice = 0;
	max_age = 0;
	for(i_voice = 0; i_voice < MODCHANS; i_voice++) {
		age = voice->age;
		if(!age) {
			/* 空きチャネル発見、即確定。 */
			sel_voice = i_voice;
			break;
		}
		if(age > max_age) {
			/* より旧いチャネル発見。 */
			sel_voice = i_voice;
			max_age = age;
			/* 継続 */
		}
		voice++;
	}

	/* 管理情報に発声をマーク。 */
	voice = &driver->voice[sel_voice];
	voice->age = 1;
	voice->channel = i_channel;
	voice->note = note;

	/*{{強調音色*/
	if(smpno & 0x80) {
		velocity <<= 2;
		smpno &= ~0x80;
		if(!smpno) DIE();
	}
	/*}}強調音色*/

	/* MODドライバに発声を指示。 */
	modch = &driver->mod.channel[sel_voice];
	modch->smpno = smpno - 1; /* MODドライバ内部では、サンプルナンバー1〜32を0〜31に置き換えて扱っています */
	modch->volume = (velocity + 1) / 2; /* velocity:0〜127 => volume:0〜64 */
	if(i_channel != 10 - 1) { /* メロディ */
		modch->period = note_period[note];
	} else {		  /* リズム */
		modch->period = note_period[RHYTHM_NOTE];
	}
	modch->offset = 0;
}

/****************************************************************************
 *	midi_note_off
 ****************************************************************************/

static void
midi_note_off(MIDIDRIVER* driver, int i_channel, int note, int velocity)
{
	MIDIVOICE* voice;
	MODCHANNEL* modch;
	int i_voice;

	/* 指定のチャネル・ノートが鳴っていたら、停止します。 */
	voice = &driver->voice[0];
	for(i_voice = 0; i_voice < MODCHANS; i_voice++) {
		if(voice->age) {
			if(voice->channel == i_channel && voice->note == note) {
				/* 管理情報に停止をマーク。 */
				voice->age = 0;
				/* MODドライバに停止を指示。 */
				modch = &driver->mod.channel[i_voice];
				modch->period = 0;
				return;
			}
		}
		voice++;
	}
}

