/*	
 *	clipsp2.c
 *
 *	P/ECE SPC Driver V2
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2004 Naoyuki Sawa
 *
 *	* Thu Oct 07 06:00:00 JST 2004 Naoyuki Sawa
 *	- 作成開始。
 */
#include "clip.h"

/****************************************************************************
 *	グローバル変数
 ****************************************************************************/

#if !defined(PIECE) || !defined(SPC2_ASM)
SPC2CPU spc2cpu; /* P/ECEの場合、高速RAMに置きます。→framsp2a.s */
#endif /*!defined(PIECE) || !defined(SPC2_ASM)*/
SPC2DSP spc2dsp;
SPC2DRV spc2drv;
unsigned char spc2ram[0x10000];	/* 0x0000〜0xffff */
unsigned char spc2ext[64];	/* 0xffc0〜0xffffの裏RAM? */

/****************************************************************************
 *	SPC2HDR
 ****************************************************************************/

int
spc2hdr_test(const void* romp)
{
	SPC2HDR* hdr = (SPC2HDR*)romp;

	/* シグネチャ検査。(minor_verは検査しません) */
	//                         000000000011111111112222222222333
	//                         012345678901234567890123456789012
	if(memcmp(hdr->signature, "SNES-SPC700 Sound File Data v0.30", 27) != 0) return -1;
	//                                                   AA
	//                                                   ||
	//                    ここまで検査することにします --++--これ以降は検査しません

	return 0;
}

/****************************************************************************
 *	SPC2DRV
 ****************************************************************************/

int
spc2drv_init(const void* romp)
{
	SPC2HDR* hdr = (SPC2HDR*)romp;
	SPC2DRV* drv = &spc2drv;
	unsigned char* ram = spc2ram;
	unsigned char* ext = spc2ext;
	//
	int i;

	/* まずクリアします。 */
	memset(drv, 0, sizeof(SPC2DRV));

	/* シグネチャ検査。 */
	if(spc2hdr_test(hdr) != 0) return -1;

	/* メモリイメージ読み込み。 */
	memcpy(ram, hdr->main_ram , 0x10000);	/* 0x0000〜0xffff */
	memcpy(ext, hdr->extra_ram,      64);	/* 0xffc0〜0xffffの裏RAM? */
	ram[0xfd] = ram[0xfe] = ram[0xff] = 0;	/* タイマカウンタリセット(WinSPCとのトレース比較を行うため。本来は不要(?)) */

	/* CPU初期化。 */
	spc2cpu_init(
		hdr->pc[0] | hdr->pc[1] << 8,
		hdr->a,
		hdr->x,
		hdr->y,
		hdr->psw,
		hdr->sp);

	/* DSP初期化。
	 * 0x4c:KEY-ONは他のレジスタに依存するため、KEY-ONだけ最後に設定します。
	 */
	spc2dsp_init();
	for(i = 0; i < 0x80; i++) {
		if(i != 0x4c) { /* KEY-ON以外 */
			spc2dsp_write(i, hdr->dsp_regs[i]);
		}
	}
	spc2dsp_write(0x4c, hdr->dsp_regs[0x4c]); /* KEY-ON */

	return 0;
}

#if !defined(PIECE) || !defined(SPC2_ASM) || defined(SPC2_DEBUG) && defined(SPC2_TRACE)
int
spc2drv_read(int addr)
{
	unsigned char* ram = spc2ram;
	//
	int data;

#ifdef SPC2_DEBUG
	if(addr < 0xf0 || addr > 0xff) DIE();
#endif /*SPC2_DEBUG*/

	switch(addr) {
	///case 0xf0: /* Test */
	///case 0xf1: /* Control */
	///case 0xf2: /* Register Address */

	case 0xf3: /* Register Data */
		/* DSPレジスタ読み出し。 */
		data = spc2dsp_read(ram[0xf2]);
		break;

	///case 0xf4: /* Port-0 */
	///case 0xf5: /* Port-1 */
	///case 0xf6: /* Port-2 */
	///case 0xf7: /* Port-3 */
	///case 0xf8: /* (空き) */
	///case 0xf9: /* (空き) */
	///case 0xfa: /* Timer-0 */
	///case 0xfb: /* Timer-1 */
	///case 0xfc: /* Timer-2 */

	case 0xfd: /* Counter-0 */
	case 0xfe: /* Counter-0 */
	case 0xff: /* Counter-0 */
		/* Counter読み出し後、クリア。 */
		data = ram[addr];
		ram[addr] = 0;
		break;

	default:
		data = ram[addr];
		break;
	}

	return data;
}
#else /*!defined(PIECE) || !defined(SPC2_ASM) || defined(SPC2_DEBUG) && defined(SPC2_TRACE)*/
/*
 * * アセンブラ版は、ふたつの手法で高速化を図っています。
 * - spc2drv_readは{addr=0x00〜0xff}の範囲でしか呼ばれないことを利用し、
 *   0xfd〜0xffの範囲チェックのうち、0xff側のチェックを省きました。
 * - タイマカウンタ読み出しがゼロだった場合、無駄ループと判断して、残りタイムスライスを放棄します。
 *   具体的には、spc2cpu_runの命令実行回数(%r3)をゼロにします。
 *   framsp2a.sのspc2cpu_run実装を参照してください。
 *   - この手法は、C言語版のspc2cpu_run(clipsp2c.c)ではなく、アセンブラ版のspc2cpu_run(framsp2a.s)が
 *     リンクされていることが前提です!!
 *     アセンブラ版のspc2cpu_runがリンクされる条件は、P/ECE環境かつSPC2_ASMが定義されていることです。
 *     この条件が満たされている場合のみ、spc2drv_read_asmを使います。
 *     また、デバッグやトレース時に命令実行回数が変わると比較しづらいので、
 *     SPC2_DEBUG,SPC2_TRACEが定義されているときは、通常版のspc2drv_readを使うことにしますq。
 *   - 呼び出し側の%r3を変更するので、確実にspc2cpu_runから呼び出されていなければいけません。
 *     誤ってspc2cpu_run以外から呼ばれないよう、ルーチン名をspc2dev_read_asmに変更しました。
 */
asm("
	.code
	.align	1
	.global	spc2drv_read_asm
spc2drv_read_asm:
	xld.w %r11, spc2ram

	xcmp %r12, 0xf3			; if(addr == 0xf3) {
	xjrne spc2drv_read_asm_L10	;
	xld.ub %r12, [%r11+0xf2]	;   return spc2dsp_read(spc2ram[0xf2]);
	xjp spc2dsp_read		; }

spc2drv_read_asm_L10:
	add %r11, %r12			; data = spc2ram[addr]
	xld.ub %r10, [%r11]

	xcmp %r12, 0xfd			; if(addr >= 0xfd) {
	xjrlt spc2drv_read_asm_L20	;
	xld.b [%r11], %r8		;   spc2ram[addr] = 0
	xcmp %r10, 0			;   if(data == 0) {
	xjrne spc2drv_read_asm_L20	;
	xld.w %r3, 0			;     残りタイムスライスを放棄 ★framsp2a.sのspc2cpu_run参照★
spc2drv_read_asm_L20:			;   }
	ret				; }
");
#endif /*!defined(PIECE) || !defined(SPC2_ASM) || defined(SPC2_DEBUG) && defined(SPC2_TRACE)*/

void
spc2drv_write(int addr, int data)
{
	unsigned char* ram = spc2ram;

#ifdef SPC2_DEBUG
	if(addr < 0xf0 || addr > 0xff) DIE();
	if(data <    0 || data > 0xff) DIE();
#endif /*SPC2_DEBUG*/

	switch(addr) {
	///case 0xf0: /* Test */

	case 0xf1: /* Control */
		/* Timer制御フラグ格納。 */
		ram[addr] = data;
		/* Portクリア。 */
		if(data & 0x10) ram[0xf4] = ram[0xf5] = 0;
		if(data & 0x20) ram[0xf6] = ram[0xf7] = 0;
		break;

	///case 0xf2: /* Register Address */
	case 0xf3: /* Register Data */
		/* DSPレジスタ書き込み。 */
		spc2dsp_write(ram[0xf2], data);
		break;

	case 0xf4: /* Port-0 */
	case 0xf5: /* Port-1 */
	case 0xf6: /* Port-2 */
	case 0xf7: /* Port-3 */
		/* Port-0〜3への書き込みは、サウンドCPUからメインCPUへの送信データになります。
		 * メインCPUからサウンドCPUへの受信データ(Port-0〜の読み出し)を変更してはいけません!!
		 * すなわち、SPCプレイヤーとしては★ここでは何もしない★のが正解です。
		 * 単純にデータを書き込んでしまうと、F-ZERO等が正しく演奏できません。
		 */
		break;

	///case 0xf8: /* (空き) */
	///case 0xf9: /* (空き) */
	///case 0xfa: /* Timer-0 */
	///case 0xfb: /* Timer-1 */
	///case 0xfc: /* Timer-2 */
	///case 0xfd: /* Counter-0 */
	///case 0xfe: /* Counter-0 */
	///case 0xff: /* Counter-0 */

	default:
		ram[addr] = data;
	}
}

void
spc2drv_mix(short wbuff[/*SPC2BUFLEN*/])
{
	SPC2DRV* drv = &spc2drv;
	unsigned char* ram = spc2ram;
	//
	int i;
	int j;
	int target;

	/* DSPミキシング処理は64サンプルづつ行います。
	 * CPU実行処理は低速タイマに同期して行います。
	 * すなわち 16[KHz:Mix]÷8[KHz:Timer]＝2[サンプル] 間隔です。
	 */
	for(i = 0; i < SPC2BUFLEN / 64; i++) {
		for(j = 0; j < 64 / 2; j++) {
			/* タイマ・カウンタを進めます。 */
			/* Timer-0 (8KHz) */
			if(ram[0xf1] & (1 << 0)) {
				if(!(target = ram[0xfa])) target = 0x100;
				drv->timer[0] += 1; /* 8[KHz]÷8[KHz]＝1 */
				while(drv->timer[0] >= target) {
					drv->timer[0] -= target;
					if(ram[0xfd] < 0xf) ram[0xfd]++;
				}
			}
			/* Timer-1 (8KHz) */
			if(ram[0xf1] & (1 << 1)) {
				if(!(target = ram[0xfb])) target = 0x100;
				drv->timer[1] += 1; /* 8[KHz]÷8[KHz]＝1 */
				while(drv->timer[1] >= target) {
					drv->timer[1] -= target;
					if(ram[0xfe] < 0xf) ram[0xfe]++;
				}
			}
			/* Timer-2 (64KHz) */
			if(ram[0xf1] & (1 << 2)) {
				if(!(target = ram[0xfc])) target = 0x100;
				drv->timer[2] += 8; /* 64[KHz]÷8[KHz]＝8 */
				while(drv->timer[2] >= target) {
					drv->timer[2] -= target;
					if(ram[0xff] < 0xf) ram[0xff]++;
				}
			}

			/* CPUを実行します。100％速度でのCPU命令実行回数は次の通りです。
			 *    2.048[MHz:CPU]÷4[Cycle/Inst,Avg]÷8[KHz] = ６４[Inst]
			 * P/ECEではとても間に合わないので、ダウンクロックしてください。
			 */
#ifndef SPC2CPU_RUN_COUNT
# ifdef PIECE
#  define SPC2CPU_RUN_COUNT 32 /* P/ECE =  50%速度 */
# else /*PIECE*/
#  define SPC2CPU_RUN_COUNT 64 /* Win32 = 100%速度 */
# endif /*PIECE*/
#endif /*SPC2CPU_RUN_COUNT*/
			spc2cpu_run(SPC2CPU_RUN_COUNT);
		}

		/* ミキシングを行います。 */
		spc2dsp_mix(wbuff);
		wbuff += 64;
	}
}

int
spc2drv_stream_callback(short* wbuff, int param)
{
	spc2drv_mix(wbuff);

	return 0;
}

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

int
spc2_play(const void* romp)
{
	/* まず確実に停止します。 */
	spc2_stop();

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

	/* ストリーム再生を開始します。 */
#ifdef SPC2_TRACE
	/* CPUトレース出力を行うと、FRAM4エリア(0x0e00〜0x1000)ではスタック不足になります。
	 * デバッグONでCPUトレース出力を行う場合は、スタック切り替えを使わないことにします。
	 */
	stream_play(SPC2BUFLEN, spc2drv_stream_callback, 0, 0/*スタック切換使わない*/);
#else /*SPC2_TRACE*/
	stream_play(SPC2BUFLEN, spc2drv_stream_callback, 0, 1/*スタック切換使用*/);
#endif /*SPC2_TRACE*/

	return 0;
}

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

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

int
spc2_inf(const void* romp, SPC2INF* inf)
{
	SPC2HDR* hdr = (SPC2HDR*)romp;
	//
	int i;
	int c;
	int mm;
	int dd;
	int yyyy;
	SPC2ID666TXT* txt;
	SPC2ID666BIN* bin;

	/* シグネチャ検査。 */
	if(spc2hdr_test(hdr) != 0) return -1;

	/* ファイル情報が要求されていなければ、成功ステータスのみを返します。 */
	if(!inf) return 0;

	/* まずクリアします。 */
	memset(inf, 0, sizeof(SPC2INF));

	/* ID666タグがなければ、全フィールドクリアのまま成功を返します。 */
	if(hdr->has_id666 != 26) return 0;

	/* ID666がテキスト形式かバイナリ形式か判別します。
	 * この方法は、SSDLaboさん(http://www.ssdlabo.jp/)のテキスト資料
	 * 「SPC ファイルの ID666 仕様書」(http://www.ssdlabo.jp/win/spcplay/ID666.txt)
	 * を参考にさせていただきました。ありがとうございます。
	 */
	if(((unsigned char*)hdr)[0xd2]) { /* テキスト形式 */
		txt = &hdr->id666.txt;
		strncpy(inf->song_title, txt->song_title, sizeof inf->song_title - 1);
		strncpy(inf->game_title, txt->game_title, sizeof inf->game_title - 1);
		strncpy(inf->dumper    , txt->dumper    , sizeof inf->dumper     - 1);
		strncpy(inf->comments  , txt->comments  , sizeof inf->comments   - 1);
		strncpy(inf->artist    , txt->artist    , sizeof inf->artist     - 1);
		strncpy(inf->date      , txt->date      , sizeof inf->date       - 1);
		for(i = 0; i < sizeof txt->play_time; i++) {
			c = txt->play_time[i];
			if(!isdigit(c)) break;
			inf->play_time = inf->play_time * 10 + (c - '0');
		}
		for(i = 0; i < sizeof txt->fade_time; i++) {
			c = txt->fade_time[i];
			if(!isdigit(c)) break;
			inf->fade_time = inf->fade_time * 10 + (c - '0');
		}
		if(txt->channel_disable) {
			//inf->channel_disable = txt->channel_disable - '0';
			//↓ (0 or 1) か ('0'or'1') か、仕様不明なので…
			inf->channel_disable = txt->channel_disable & 1;
		}
		if(txt->emulator) {
			inf->emulator = txt->emulator - '0';
		}
	} else { /* バイナリ形式 */
		bin = &hdr->id666.bin;
		strncpy(inf->song_title, bin->song_title, sizeof inf->song_title - 1);
		strncpy(inf->game_title, bin->game_title, sizeof inf->game_title - 1);
		strncpy(inf->dumper    , bin->dumper    , sizeof inf->dumper     - 1);
		strncpy(inf->comments  , bin->comments  , sizeof inf->comments   - 1);
		strncpy(inf->artist    , bin->artist    , sizeof inf->artist     - 1);
		mm   = bin->date[1];
		dd   = bin->date[0];
		yyyy = bin->date[2] | bin->date[3] << 8;
		if(mm || dd || yyyy) {
			sprintf(inf->date, "%02d/%02d/%04d", mm % 100, dd % 100, yyyy % 10000);
		}
		for(i = 0; i < sizeof bin->play_time; i++) {
			c = bin->play_time[i];
			inf->play_time += c << (8 * i);
		}
		for(i = 0; i < sizeof bin->fade_time; i++) {
			c = bin->fade_time[i];
			inf->fade_time += c << (8 * i);
		}
		inf->channel_disable = bin->channel_disable;
		inf->emulator = bin->emulator;
	}

	return 0;
}
