/*
*	Name:		OPL2/OPL3 Music driver
*	Project:	MUS File Player Library
*	Version:	1.67
*	Author:		Vladimir Arnost (QA-Software)
*	Last revision:	May-2-1996
*	Compiler:	Borland C++ 3.1, Watcom C/C++ 10.0
*
*/

/*
* Revision History:
*
*	Aug-8-1994	V1.00	V.Arnost
*		Written from scratch
*	Aug-9-1994	V1.10	V.Arnost
*		Some minor changes to improve sound quality. Tried to add
*		stereo sound capabilities, but failed to -- my SB Pro refuses
*		to switch to stereo mode.
*	Aug-13-1994	V1.20	V.Arnost
*		Stereo sound fixed. Now works also with Sound Blaster Pro II
*		(chip OPL3 -- gives 18 "stereo" (ahem) channels).
*		Changed code to handle properly notes without volume.
*		(Uses previous volume on given channel.)
*		Added cyclic channel usage to avoid annoying clicking noise.
*	Aug-28-1994	V1.40	V.Arnost
*		Added Adlib and SB Pro II detection.
*	Apr-16-1995	V1.60	V.Arnost
*		Moved into separate source file MUSLIB.C
*	Jul-12-1995	V1.62	V.Arnost
*		Module created and source code copied from MUSLIB.C
*	Aug-08-1995	V1.63	V.Arnost
*		Modified to follow changes in MLOPL_IO.C
*	Aug-13-1995	V1.64	V.Arnost
*		Added OPLsendMIDI() function
*	Sep-8-1995	V1.65	V.Arnost
*		Added sustain pedal support.
*		Improved pause/unpause functions. Now all notes are made
*		silent (even released notes, which haven't sounded off yet).
*	Mar-1-1996	V1.66	V.Arnost
*		Cleaned up the source
*	May-2-1996	V1.67	V.Arnost
*		Added modulation wheel (vibrato) support
*/

#include <malloc.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#endif
#include "muslib.h"

#include "c_cvars.h"

#define MOD_MIN		40		/* vibrato threshold */


//#define HIGHEST_NOTE 102
#define HIGHEST_NOTE 127

musicBlock::musicBlock ()
{
	memset (this, 0, sizeof(*this));
}

musicBlock::~musicBlock ()
{
	if (OPLinstruments != NULL) free(OPLinstruments);
}

void musicBlock::writeFrequency(uint slot, uint note, int pitch, uint keyOn)
{
	io->OPLwriteFreq (slot, note, pitch, keyOn);
}

void musicBlock::writeModulation(uint slot, struct OPL2instrument *instr, int state)
{
	if (state)
		state = 0x40;	/* enable Frequency Vibrato */
	io->OPLwriteChannel(0x20, slot,
		(instr->feedback & 1) ? (instr->trem_vibr_1 | state) : instr->trem_vibr_1,
		instr->trem_vibr_2 | state);
}

uint musicBlock::calcVolume(uint channelVolume, uint MUSvolume, uint noteVolume)
{
	noteVolume = ((ulong)channelVolume * MUSvolume * noteVolume) / (256*127);
	if (noteVolume > 127)
		return 127;
	else
		return noteVolume;
}

int musicBlock::occupyChannel(uint slot, uint channel,
						 int note, int volume, struct OP2instrEntry *instrument, uchar secondary)
{
	struct OPL2instrument *instr;
	struct OPLdata *data = &driverdata;
	struct channelEntry *ch = &channels[slot];

	ch->channel = channel;
	ch->note = note;
	ch->flags = secondary ? CH_SECONDARY : 0;
	if (data->channelModulation[channel] >= MOD_MIN)
		ch->flags |= CH_VIBRATO;
	ch->time = MLtime;
	if (volume == -1)
		volume = data->channelLastVolume[channel];
	else
		data->channelLastVolume[channel] = volume;
	ch->realvolume = calcVolume(data->channelVolume[channel], 256, ch->volume = volume);
	if (instrument->flags & FL_FIXED_PITCH)
		note = instrument->note;
	else if (channel == PERCUSSION)
		note = 60;			// C-5
	if (secondary && (instrument->flags & FL_DOUBLE_VOICE))
		ch->finetune = (instrument->finetune - 0x80) >> 1;
	else
		ch->finetune = 0;
	ch->pitch = ch->finetune + data->channelPitch[channel];
	if (secondary)
		instr = &instrument->instr[1];
	else
		instr = &instrument->instr[0];
	ch->instr = instr;
	if (channel != PERCUSSION && !(instrument->flags & FL_FIXED_PITCH))
	{
		if ( (note += instr->basenote) < 0)
			while ((note += 12) < 0) {}
		else if (note > HIGHEST_NOTE)
			while ((note -= 12) > HIGHEST_NOTE) {}
	}
	ch->realnote = note;

	io->OPLwriteInstrument(slot, instr);
	if (ch->flags & CH_VIBRATO)
		writeModulation(slot, instr, 1);
	io->OPLwritePan(slot, instr, data->channelPan[channel]);
	io->OPLwriteVolume(slot, instr, ch->realvolume);
	writeFrequency(slot, note, ch->pitch, 1);
	return slot;
}

int musicBlock::releaseChannel(uint slot, uint killed)
{
	struct channelEntry *ch = &channels[slot];
	writeFrequency(slot, ch->realnote, ch->pitch, 0);
	ch->channel |= CH_FREE;
	ch->time = MLtime;
	ch->flags = CH_FREE;
	if (killed)
	{
		io->OPLwriteChannel(0x80, slot, 0x0F, 0x0F);  // release rate - fastest
		io->OPLwriteChannel(0x40, slot, 0x3F, 0x3F);  // no volume
	}
	return slot;
}

int musicBlock::releaseSustain(uint channel)
{
	uint i;
	uint id = channel;

	for(i = 0; i < io->OPLchannels; i++)
	{
		if (channels[i].channel == id && channels[i].flags & CH_SUSTAIN)
			releaseChannel(i, 0);
	}
	return 0;
}

int musicBlock::findFreeChannel(uint flag, uint channel, uchar note)
{
	uint i;

	ulong bestfit = 0;
	uint bestvoice = 0;

	for (i = 0; i < io->OPLchannels; ++i)
	{
		ulong magic;

		magic = ((channels[i].flags & CH_FREE) << 24) |
				((channels[i].note == note &&
					channels[i].channel == channel) << 30) |
				((channels[i].flags & CH_SUSTAIN) << 28) |
				((MLtime - channels[i].time) & 0x1fffffff);
		if (magic > bestfit)
		{
			bestfit = magic;
			bestvoice = i;
		}
	}
	if ((flag & 1) && !(bestfit & 0x80000000))
	{ // No free channels good enough
		return -1;
	}
	releaseChannel (bestvoice, 1);
	return bestvoice;
}

struct OP2instrEntry *musicBlock::getInstrument(uint channel, uchar note)
{
	uint instrnumber;

	if (channel == PERCUSSION)
	{
		if (note < 35 || note > 81)
			return NULL;		/* wrong percussion number */
		instrnumber = note + (128-35);
	} else
	{
		instrnumber = driverdata.channelInstr[channel];
	}

	if (OPLinstruments)
		return &OPLinstruments[instrnumber];
	else
		return NULL;
}


// code 1: play note
CVAR (Bool, opl_singlevoice, 0, 0)

void musicBlock::OPLplayNote(uint channel, uchar note, int volume)
{
	int i;
	struct OP2instrEntry *instr;

	if (volume == 0)
	{
		OPLreleaseNote (channel, note);
		return;
	}

	if ( (instr = getInstrument(channel, note)) == NULL )
		return;

	if ( (i = findFreeChannel((channel == PERCUSSION) ? 2 : 0, channel, note)) != -1)
	{
		occupyChannel(i, channel, note, volume, instr, 0);
		if ((instr->flags & FL_DOUBLE_VOICE) && !opl_singlevoice)
		{
			if ( (i = findFreeChannel((channel == PERCUSSION) ? 3 : 1, channel, note)) != -1)
				occupyChannel(i, channel, note, volume, instr, 1);
		}
	}
}

// code 0: release note
void musicBlock::OPLreleaseNote(uint channel, uchar note)
{
	uint i;
	uint id = channel;
	struct OPLdata *data = &driverdata;
	uint sustain = data->channelSustain[channel];

	for(i = 0; i < io->OPLchannels; i++)
	{
		if (channels[i].channel == id && channels[i].note == note)
		{
			if (sustain < 0x40)
				releaseChannel(i, 0);
			else
				channels[i].flags |= CH_SUSTAIN;
		}
	}
}

// code 2: change pitch wheel (bender)
void musicBlock::OPLpitchWheel(uint channel, int pitch)
{
	uint i;
	uint id = channel;
	struct OPLdata *data = &driverdata;

	//pitch -= 0x80;
	pitch >>= 1;
	data->channelPitch[channel] = pitch;
	for(i = 0; i < io->OPLchannels; i++)
	{
		struct channelEntry *ch = &channels[i];
		if (ch->channel == id)
		{
			ch->time = MLtime;
			ch->pitch = ch->finetune + pitch;
			writeFrequency(i, ch->realnote, ch->pitch, 1);
		}
	}
}

// code 4: change control
void musicBlock::OPLchangeControl(uint channel, uchar controller, int value)
{
	uint i;
	uint id = channel;
	struct OPLdata *data = &driverdata;

	switch (controller)
	{
	case ctrlPatch:			/* change instrument */
		data->channelInstr[channel] = value;
		break;
	case ctrlModulation:
		data->channelModulation[channel] = value;
		for(i = 0; i < io->OPLchannels; i++)
		{
			struct channelEntry *ch = &channels[i];
			if (ch->channel == id)
			{
				uchar flags = ch->flags;
				ch->time = MLtime;
				if (value >= MOD_MIN)
				{
					ch->flags |= CH_VIBRATO;
					if (ch->flags != flags)
						writeModulation(i, ch->instr, 1);
				} else {
					ch->flags &= ~CH_VIBRATO;
					if (ch->flags != flags)
						writeModulation(i, ch->instr, 0);
				}
			}
		}
		break;
	case ctrlVolume:		/* change volume */
		data->channelVolume[channel] = value;
		for(i = 0; i < io->OPLchannels; i++)
		{
			struct channelEntry *ch = &channels[i];
			if (ch->channel == id)
			{
				ch->time = MLtime;
				ch->realvolume = calcVolume(value, 256, ch->volume);
				io->OPLwriteVolume(i, ch->instr, ch->realvolume);
			}
		}
		break;
	case ctrlPan:			/* change pan (balance) */
		data->channelPan[channel] = value -= 64;
		for(i = 0; i < io->OPLchannels; i++)
		{
			struct channelEntry *ch = &channels[i];
			if (ch->channel == id)
			{
				ch->time = MLtime;
				io->OPLwritePan(i, ch->instr, value);
			}
		}
		break;
	case ctrlSustainPedal:		/* change sustain pedal (hold) */
		data->channelSustain[channel] = value;
		if (value < 0x40)
			releaseSustain(channel);
		break;
	}
}


void musicBlock::OPLplayMusic()
{
	uint i;
	struct OPLdata *data = &driverdata;

	for (i = 0; i < CHANNELS; i++)
	{
		data->channelVolume[i] = 127;	/* default volume 127 (full volume) */
		data->channelSustain[i] = 0;
		data->channelLastVolume[i] = 64;
		data->channelPitch[i] = 64;
	}
}

void musicBlock::OPLstopMusic()
{
	uint i;
	for(i = 0; i < io->OPLchannels; i++)
		if (!(channels[i].flags & CH_FREE))
			releaseChannel(i, 1);
}

void musicBlock::OPLchangeVolume(uint volume)
{
	uchar *channelVolume = driverdata.channelVolume;
	uint i;
	for(i = 0; i < io->OPLchannels; i++)
	{
		struct channelEntry *ch = &channels[i];
		ch->realvolume = calcVolume(channelVolume[ch->channel & 0xF], volume, ch->volume);
		io->OPLwriteVolume(i, ch->instr, ch->realvolume);
	}
}

int musicBlock::OPLloadBank (FileReader &data)
{
	static const uchar masterhdr[8] = { '#','O','P','L','_','I','I','#' };
	struct OP2instrEntry *instruments;

	uchar filehdr[8];

	data.Read (filehdr, 8);
	if (memcmp(filehdr, masterhdr, 8))
		return -2;			/* bad instrument file */
	if ( (instruments = (struct OP2instrEntry *)calloc(OP2INSTRCOUNT, OP2INSTRSIZE)) == NULL)
		return -3;			/* not enough memory */
	data.Read (instruments, OP2INSTRSIZE * OP2INSTRCOUNT);
	if (OPLinstruments != NULL)
	{
		free(OPLinstruments);
	}
	OPLinstruments = instruments;
#if 0
	for (int i = 0; i < 175; ++i)
	{
		Printf ("%3d.%-33s%3d %3d %3d %d\n", i,
			(BYTE *)data+6308+i*32,
			OPLinstruments[i].instr[0].basenote,
			OPLinstruments[i].instr[1].basenote,
			OPLinstruments[i].note,
			OPLinstruments[i].flags);
	}
#endif
	return 0;
}
