/*	
 *	clippsid.c
 *
 *	P/ECE PSID Driver
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2003 Naoyuki Sawa
 *
 *	* Sat Dec 27 06:00:00 JST 2003 Naoyuki Sawa
 *	- 作成開始。
 */
#include "clip.h"

/****************************************************************************
 *	M6502エミュレーション用外部関数
 ****************************************************************************/

void
psid_m6502_out(M6502* m6502, unsigned short addr, unsigned char data)
{
	PSIDDRIVER* psid = (PSIDDRIVER*)m6502;
	int regno;

	/* SID */
	if(addr >= 0xd400 && addr <= 0xd7ff) {
		regno = addr & 0x1f;
		if(regno <= 0x1c) {
			/* Real SID Chip */
			sid_write(&psid->sid, regno, data);
		} else {
			/* PlaySID Extended SID */
			psid_xsid_write(psid, addr, data);
		}
		return;
	}

	/* CIA#1 Timer A */
	if(addr >= 0xdc00 && addr <= 0xdcff) {
		if(!psid->speed) return; /* 0:PAL(1/50[秒])固定 => タイミング変更不可 */
		regno = addr & 0xf;
		switch(regno) {
		case 0x4:
			((unsigned char*)&psid->cia_timer)[0] = data;
			break;
		case 0x5:
			((unsigned char*)&psid->cia_timer)[1] = data;
			break;
		}
		return;
	}
}

/****************************************************************************
 *	拡張SID内部関数
 ****************************************************************************/

void
psid_xsid_write(PSIDDRIVER* psid, unsigned short addr, unsigned char data)
{
	XSID* xsid = &psid->xsid;
	unsigned char* reg = xsid->reg;
	int regno = addr - 0xd400;

	switch(regno) {
	case 0x1d: /* Galway-Noise(START) / Sample(START/STOP) */
		switch(data) {
		case 0xfd: /* Sample(STOP) */
			xsid->state = 0;
			break;
		case 0xfc: /* Sample(START) Vol=1/4 */
		case 0xfe: /* Sample(START) Vol=1/2 */
		case 0xff: /* Sample(START) Vol=1/1 */
			psid_xsid_sample_start(psid, data);
			break;
		default: /* Galway-Noise(START) */
			psid_xsid_galway_noise_start(psid, data);
			break;
		}
		break;
	case 0x1e: /* Galway-Noise(ToneData addr Lo) / Sample(SampleData addr Lo) */
	case 0x1f: /* Galway-Noise(ToneData addr Hi) / Sample(SampleData addr Hi) */
	case 0x3d: /* Galway-Noise(Tonelength) / Sample(SampleData end addr Lo) */
	case 0x3e: /* Galway-Noise(Volume) / Sample(SampleData end addr Hi) */
	case 0x3f: /* Galway-Noise(Period for each value) / Sample(Repeat) */
	case 0x5d: /* Galway-Noise(Period for value 0) / Sample(Period Lo) */
	case 0x5e: /* Sample(Period Hi) */
	case 0x5f: /* Sample(Number of bytes to add after Reading one nibble) */
	case 0x7d: /* Sample(Sampleorder) */
	case 0x7e: /* Sample(SampleData repeat addr Lo) */
	case 0x7f: /* Sample(SampleData repeat addr Hi) */
		reg[regno] = data;
		break;
	default:
		DIE();
	}
}

void
psid_xsid_galway_noise_start(PSIDDRIVER* psid, unsigned char data)
{
	XSID* xsid = &psid->xsid;
	unsigned char* reg = xsid->reg;
	XSIDGN* gn = &xsid->context.gn;
	unsigned char* mem = psid->mem;

	xsid->state = XSID_GALWAY_NOISE;
	gn->tones	= data;				/* data〜0までのダウンカウンタ */
	gn->addr	=  reg[0x1e] |			/* mem[addr+tones]〜mem[addr+0]の順に使います */
			   reg[0x1f] << 8;
	gn->len		=  reg[0x3d];			/* 各Toneの継続クロック数 */
	gn->vol		= (reg[0x3e] & 0xf) * 3;	/* 符号なし5.5bit (出力が17.5bitになるように調整) */
	gn->period1	=  reg[0x3f];			/* period = addr[tones] * period1 + period0 */
	gn->period0	=  reg[0x5d];

	/* 無効パラメタのチェック。 */
	if(!gn->vol || !gn->len || !gn->period1 || !gn->period0) {
		xsid->state = 0; /* 発声キャンセル */
		return;
	}

	/* 最初のトーン設定。 */
	gn->progress = 0;
	gn->period = mem[gn->addr + gn->tones] * gn->period1 + gn->period0;
	gn->count = gn->len;
}

void
psid_xsid_sample_start(PSIDDRIVER* psid, unsigned char data)
{
	XSID* xsid = &psid->xsid;
	unsigned char* reg = xsid->reg;
	XSIDSM* sm = &xsid->context.sm;

	xsid->state = XSID_SAMPLE;
	switch(data) { /* Volume=1/1時に出力が符号付き17.5bit(=16bit×3)になるように設定 */
	case 0xfc:        sm->vol = (1 << (16 - 4/*SampleData*/)) * 3 >> 2; break;
	case 0xfe:        sm->vol = (1 << (16 - 4/*SampleData*/)) * 3 >> 1; break;
	default: /*0xff*/ sm->vol = (1 << (16 - 4/*SampleData*/)) * 3 >> 0; break;
	}
	sm->addr	=     (reg[0x1e] |
			       reg[0x1f] << 8) << 1/*Nibble addr*/;
	sm->addr_end	=     (reg[0x3d] |
			       reg[0x3e] << 8) << 1/*Nibble addr*/;
	sm->rep_cnt	=      reg[0x3f];
	sm->period	=      reg[0x5d] |
			       reg[0x5e] << 8;
	sm->stride	= 1 << reg[0x5f];
	sm->order	=      reg[0x7d];
	sm->addr_rep	=     (reg[0x7e] |
			       reg[0x7f] << 8) << 1/*Nibble addr*/;
	sm->dda_d	= -sm->period;
}

int
psid_xsid_process(PSIDDRIVER* psid, int step)
{
	XSID* xsid = &psid->xsid;
	int value;

	switch(xsid->state) {
	case XSID_GALWAY_NOISE:
		value = psid_xsid_galway_noise_process(psid, step);
		break;
	case XSID_SAMPLE:
		value = psid_xsid_sample_process(psid, step);
		break;
	default:
		value = 0;
		break;
	}

	return value;
}

int
psid_xsid_galway_noise_process(PSIDDRIVER* psid, int step)
{
	int value;
	XSID* xsid = &psid->xsid;
	unsigned char* mem = psid->mem;
	XSIDGN* gn = &xsid->context.gn;

	/* クロックを進めます。 */
	gn->progress += step;
	while(gn->progress >= gn->period) { /* 1波形終了? */
		gn->progress -= gn->period;
		gn->count--;
		if(!gn->count) { /* トーン終了? */
			if(!gn->tones) { /* 発声終了? */
				xsid->state = 0;
				return 0;
			}
			gn->tones--;
			/* 次のトーン設定。 */
			/* (progressは継続) */
			gn->period = mem[gn->addr + gn->tones] * gn->period1 + gn->period0;
			gn->count = gn->len;
		}
	}

	/* Galway-Noiseの波形はノコギリ波固定みたいです。(たぶん) */
	/* period1<>0,period0<>0なので、period<>0が保証されています。除算は安全です。*/
	/* あまり使用頻度高くないし1チャネルしかないから、除算使ってもまあいいかな？ */
	value = ((gn->progress << 12) / gn->period - (1 << 11))	/*   符号つき12  bit */
	        * gn->vol;					/* ×符号なし 5.5bit */
								/* ＝符号つき17.5bit */
	return value;
}

int
psid_xsid_sample_process(PSIDDRIVER* psid, int step)
{
	int value;
	XSID* xsid = &psid->xsid;
	unsigned char* mem = psid->mem;
	XSIDSM* sm = &xsid->context.sm;

	/* 出力サンプルを求めます。 */
	value = mem[sm->addr >> 1];
	if((sm->addr & 1) & sm->order) {
		/* {Lo,Hi}[1] or {Hi,Lo}[0] */
		value >>= 4;
	} else {
		/* {Lo,Hi}[0] or {Hi,Lo}[1] */
		value &= 0xf;
	}
	value = (value - 8)	/* 0〜15 => -8〜7 */
	        * sm->vol;	/* -8〜7 => 符号付き17.5bit */

	/* サンプルデータアドレスを進めます。 */
	sm->dda_d += step;
	while(sm->dda_d >= 0) {
		sm->dda_d -= sm->period;
		sm->addr += sm->stride;
	}
	while(sm->addr >= sm->addr_end) {
		if(sm->rep_cnt == 0) {
			xsid->state = 0;
			return 0;
		}
		if(sm->rep_cnt != 0xff/*∞*/) {
			sm->rep_cnt--;
		}
		sm->addr -= sm->addr_end - sm->addr_rep;
	}

	return value;
}

/****************************************************************************
 *	PSIDドライバ内部関数
 ****************************************************************************/

/* 初期化ルーチン/演奏ルーチンの呼び出し元プログラム */
static const unsigned char psid_program[] = {
	'\x20',		/* +0: JSR Absolute */
	'\x00',		/* +1: ここに呼び出し先アドレスの下位バイトをセット */
	'\x00',		/* +2: ここに呼び出し先アドレスの上位バイトをセット */
	'\xcb',		/* +3: WAI */
};			/* =4 */
/* 呼び出し元プログラムを転送するアドレス
 * Kernel ROM area (0xe000〜0xffff) の真ん中が使われないと仮定して、0xf000〜に置くことにします。
 * ↓2003/12/25
 * バンク切替時は0xe000〜0xffffもRAMになり、0xf000〜辺りまでユーザープログラムがロードされる可能性があるので、
 * 安全のためにもっと後ろ、ベクタテーブル(0xfffa〜0xffff)の直前近くへずらすことにしました。
 */
#define PSID_PROGRAM_ADDRESS	0xfff0 /* 調整可 */
/* 呼び出し先(初期化ルーチン/演奏ルーチン)アドレスをセットするアドレス
 * 呼び出し元プログラムの「JSR Absolute」命令のオペランドとなります。
 */
#define PSID_RUN_PC_ADDRESS	(PSID_PROGRAM_ADDRESS + 1)

void
psid_m6502_setup(PSIDDRIVER* psid, int a, int pc)
{
	M6502* m6502 = &psid->m6502;
	unsigned char* mem = psid->mem;

	/* 呼び出し元プログラム経由で、呼び出し先プログラムを呼び出します。 */
	mem[PSID_RUN_PC_ADDRESS + 0] = pc & 0xff;
	mem[PSID_RUN_PC_ADDRESS + 1] = pc >> 8;
	m6502->a = a;
	m6502->pc = PSID_PROGRAM_ADDRESS;
	m6502->sp = 0xff;
	m6502->wait = 0;  /* 必要!! */
	m6502->cycle = 0; /* 必要!! */
}

int
psid_init(PSIDDRIVER* psid, const void* _data, int len, int i_song)
{
	M6502* m6502 = &psid->m6502;
	unsigned char* mem = psid->mem;
	SID* sid = &psid->sid;
	//
	unsigned char* data = (unsigned char*)_data;
	PSIDHEADER* header = (PSIDHEADER*)_data;
	int data_offset;
	int load_addr;
	int init_addr;
	int play_addr;
	int songs;
	int start_song;
	int speed;
	//
	int n1, n2;

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

	/* メインメモリを初期化します。 */
	memset(&mem[0x0000], 0x00, 0xe000);
	memset(&mem[0xe000], 0x40, 0x2000);	/* BASIC/Kernal ROM <= 0x40:RTI */
	mem[0x0001] = 7;			/* 6510 On-chip I/O Register <= 7 (Bank-sel) */
	mem[0x02a6] = 1;			/* 0:NTSC/1:PAL (これを見ている演奏ルーチンはないような気がするが…) */
	/* ※既存SIDプレイヤー(SIDPlayerなど)に倣って、ベクタアドレスの初期値を格納しておきます。
	 *   が、この作業に意味があるとは思えません。
	 *   だって、このベクタアドレスの先に割り込み処理ルーチンはないのですから・・・
	 *   削ってしまってもいいと思うのですが、念のために一応残してあります。
	 */
	mem[0x0314] = 0x31;			/* IRQ Interrupt Address <= 0xea31 (Default) */
	mem[0x0315] = 0xea;
	mem[0x0316] = 0x66;			/* BRK Interrupt Address <= 0xfe66 (Default) */
	mem[0x0317] = 0xfe;
	mem[0x0318] = 0x47;			/* NMI Interrupt Address <= 0xfe66 (Default) */
	mem[0x0319] = 0xfe;

	/* 初期化ルーチン/演奏ルーチン呼び出し元プログラムを転送します。 */
	memcpy(&mem[PSID_PROGRAM_ADDRESS], psid_program, sizeof psid_program);

	/* ファイルヘッダを解析します。 */
	if(memcmp(header->signature, "PSID", 4) != 0) return -1; /* シグネチャ検査 */
	/* versionは使いません。 */
	data_offset = PSID_HALF(header->data_offset);
	load_addr = PSID_HALF(header->load_addr);
	init_addr = PSID_HALF(header->init_addr);
	play_addr = PSID_HALF(header->play_addr);
	songs = PSID_HALF(header->songs);
	start_song = PSID_HALF(header->start_song);
	speed = PSID_WORD(header->speed);
	/* nameは使いません。 */
	/* authorは使いません。 */
	/* copyrightは使いません。 */
	/* flagsは使いません。 */
	/* reservedは使いません。 */

	/* 演奏する曲番号を決定します。 */
	if(!songs) songs = 1;			/* 曲数0 => 曲数1と解釈する仕様 */
	if(start_song) start_song--;		/* PSIDHEADER.start_song(1ベース) => init_addr引数(0ベース) */
	if(i_song < 0) i_song = start_song;	/* 引数による曲番号指定無し(i_song<0)なら、start_songを使用 */
	if(i_song < 0 || songs - 1 < i_song) return -1;

	/* メモリイメージを読み込みます。 */
	if(!load_addr) {
		/* load_addr=NULLの場合は、メモリイメージの最初にロードアドレスが記されています。 */
		load_addr = data[data_offset + 0] | data[data_offset + 1] << 8; /* Little-endian! */
		data_offset += 2;
	}
	n1 = len - data_offset;			/* メモリイメージサイズ */
	n2 = PSID_PROGRAM_ADDRESS - load_addr;	/* ロード可能なサイズ */
	if(n1 > n2) return -1;
	memcpy(&mem[load_addr], &data[data_offset], n1);

	/* SIDリセット。 */
	sid_reset(sid, PSID_C64_CLOCK);
	/* 拡張SIDはリセット処理不要です。 */

	/* CIA#1 Timer A 初期値設定。
	 * * SIDファイルのspeedフィールドの値によって、設定値を変えます。
	 * - speedフィールドの意味は次の通りです。
	 *	bit 0:  1曲目の演奏タイミング= 0:PAL固定 / 1:CIA#1 Timer A 可変
	 *	bit 1:  2曲目の演奏タイミング= 0:PAL固定 / 1:CIA#1 Timer A 可変
	 *	...
	 *	bit31: 32曲目の演奏タイミング= 0:PAL固定 / 1:CIA#1 Timer A 可変
	 * - SIDファイルの仕様では、PALタイミング時はCIAを使わずに1/50[秒]間隔で演奏ルーチンを呼ぶことになっていますが、
	 *   本プログラムは簡略化のために、PAL/CIAどちらの場合もCIAを使って演奏ルーチン呼び出しタイミングを制御しています。
	 * - PALタイミングの場合はCIAを1/50[秒]に設定し、初期化/演奏ルーチンからのCIA再設定を無効にしておきます。
	 *   こうすることにより、1/50[秒]固定タイミングで演奏ルーチンが呼ばれます。
	 * - SIDタイミングの場合は、CIAの初期値は1/60[秒]です。(C64システムがPALかNTSCかに関係なく1/60[秒]です!!重要!!)
	 *   初期化/演奏ルーチンがCIAを再設定したら、その変更を受け入れます。
	 *   こうすることにより、初期値1/60[秒]または可変タイミングで演奏ルーチンが呼ばれます。
	 */
	psid->speed = (speed >> i_song) & 1;
	if(!psid->speed) {
		psid->cia_timer = PSID_C64_CLOCK / 50;	/* PAL(1/50[秒])固定 */
	} else {
		psid->cia_timer = PSID_C64_CLOCK / 60;	/* CIA(1/60[秒])可変 */
	}
	/* psid_mix()の初回ループでIRQ発生するように、cia_countの初期値は0としておきます。(memsetでクリア済み) */

	/* CPUリセット。 */
	//m6502_reset(m6502, psid_m6502_read, psid_m6502_write);
	//↓※I/O READ 省略
	m6502_reset(m6502, NULL, psid_m6502_write);

	/* 初期化ルーチン実行。 */
	if(!init_addr) {
		/* init_addr=NULLの場合は、メモリイメージの先頭に初期化ルーチンがあります。 */
		init_addr = load_addr;
	}
	psid_m6502_setup(psid, i_song, init_addr);
	m6502_run(m6502, PSID_C64_CLOCK);	/* 1秒以内に完了すると仮定  */
	if(!m6502->wait) return -1;		/* 完了しなければエラー扱い */

	/* 演奏ルーチンアドレス決定。 */
	if(!play_addr) {
		/* play_addr=NULLの場合は、初期化ルーチンが割り込みベクタを設定しています。 */
		if(mem[0x0001] & 2) {	/* Kernal ROM switched in */
			play_addr = mem[0x0314] | mem[0x0315] << 8; /* $0314: IRQ Hook   (Software) */
		} else {		/* Kernal ROM switched out */
			play_addr = mem[0xfffe] | mem[0xffff] << 8; /* $fffe: IRQ Vector (Hardware) */
		}
	}
	psid->play_addr = play_addr;

	return 0;
}

int
psid_stream_callback(short* wbuff, int param)
{
	return psid_mix((PSIDDRIVER*)param, wbuff);
}

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

int
psid_play(const void* data, int len, int i_song)
{
	static PSIDDRIVER psid; /* STATICです! */

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

	/* PSIDドライバを初期化します。 */
	if(psid_init(&psid, data, len, i_song) != 0) return -1;

	/* ストリーム再生を開始します。 */
	stream_play(PSIDBUFLEN, psid_stream_callback, (int)&psid, 1/*スタック切換使用*/);

	return 0;
}

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

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

