// ###########################################################################
// #
// # OSCAAudio - Implementation of OSCA Audio Emulation Class
// # Copyright (C) 2009-2011 Tony Friery [DialTone]
// #
// # ALL RIGHTS RESERVED
// #
// ###########################################################################

#pragma hdrstop

#include <stdio.h>
#include "OSCAAudio.h"
#include "FormDiag.h"
#include "OSCABoard.h"

//---------------------------------------------------------------------------

#define BUFFLEN					24000
#define INTERNAL_SAMPLE_RATE 	48000

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::AppCreateBasicBuffer
//
//  PURPOSE:	Create a DirectSound Audio Buffer (INTERNAL USE ONLY)
//
//  INPUTS:		LPDIRECTSOUNDBUFFER *	Pointer to buffer pointer
//					WAVEFORMATEX				Format of buffer to create
//					DWORD							Size of buffer to create
//
//  OUTPUT:		bool							true on success otherwise false
//
///////////////////////////////////////////////////////////////////////
bool __fastcall TOSCAAudioSystem::AppCreateBasicBuffer(
		  LPDIRECTSOUNDBUFFER *lplpDsb,
		  WAVEFORMATEX &wfx,
		  DWORD length)
{
	 DSBUFFERDESC dsbdesc;
	 HRESULT hr;

	 // Set up DSBUFFERDESC structure.
	 memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); // Zero it out.

	 dsbdesc.dwSize = sizeof(DSBUFFERDESC);
	 dsbdesc.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2 |
		DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLPAN;
	 dsbdesc.dwBufferBytes = length;
	 dsbdesc.lpwfxFormat = &wfx;

	 // Create buffer.
	 hr = m_lpds->CreateSoundBuffer(&dsbdesc, lplpDsb, NULL);
	 if SUCCEEDED(hr)
	 {
		  // Valid interface is in *lplpDsb.
		  return true;
	 }
	 else
	 {
		  // Failed.
		  *lplpDsb = NULL;
		  return false;
	 }
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::AppWriteDataToBuffer
//
//  PURPOSE:	Write to DirectSound Audio Buffer (INTERNAL USE ONLY)
//
//  INPUTS:		LPDIRECTSOUNDBUFFER 		Buffer pointer
//					DWORD							Offset into buffer to begin write
//					LPBYTE						Pointer to data to be written
//					DWORD							Length of data to write (bytes)
//
//  OUTPUT:		bool							true on success otherwise false
//
///////////////////////////////////////////////////////////////////////
bool __fastcall TOSCAAudioSystem::AppWriteDataToBuffer(
	 LPDIRECTSOUNDBUFFER lpDsb,  // the DirectSound buffer
	 DWORD dwOffset,             // our own write cursor
	 LPBYTE lpbSoundData,        // start of our data
	 DWORD dwSoundBytes)         // size of block to copy
{
	LPVOID  lpvPtr1;
	DWORD   dwBytes1;
	LPVOID  lpvPtr2;
	DWORD   dwBytes2;
	HRESULT hr;
	DWORD  dwAudio1, dwAudio2;

	// Obtain memory address of write block. This will be in two parts
	// if the block wraps around.
	hr = lpDsb->Lock(dwOffset, dwSoundBytes, &lpvPtr1,
		&dwBytes1, &lpvPtr2, &dwBytes2, 0);

	// If DSERR_BUFFERLOST is returned, restore and retry lock.
	if (DSERR_BUFFERLOST == hr)
	{
		lpDsb->Restore();
		hr = lpDsb->Lock(dwOffset, dwSoundBytes,
			&lpvPtr1, &dwAudio1, &lpvPtr2, &dwAudio2, 0);
	}

	if SUCCEEDED(hr)
	{
		// Write to pointers.
		CopyMemory(lpvPtr1, lpbSoundData, dwBytes1);

		if (NULL != lpvPtr2)
		{
			CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2);
		}

		// Release the data back to DirectSound.
		hr = lpDsb->Unlock(lpvPtr1, dwBytes1, lpvPtr2, dwBytes2);
		if SUCCEEDED(hr)
		{
				// Success.
			return true;
		}
	}

	// Lock, Unlock, or Restore failed.
	return false;
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::InitSound
//
//  PURPOSE:	Initialize sound subsystems - DirectSound and Mixer
//
//  INPUTS:		void
//
//  OUTPUT:		bool						true on success
//
///////////////////////////////////////////////////////////////////////
bool __fastcall TOSCAAudioSystem::InitSound(void)
{
	WAVEFORMATEX wfx;
	DSBUFFERDESC dsbdesc;

	// If the window handle is NULL, then it'll be impossible to
	// initialise DirectSound
	if (m_hHandle == NULL)
	{
		return false;
	}

	// First Initialize Direct Sound
	FillMemory(&wfx, sizeof(WAVEFORMATEX), 0x00);
	wfx.wFormatTag = WAVE_FORMAT_PCM;
	wfx.nChannels = 2;
	wfx.nSamplesPerSec = INTERNAL_SAMPLE_RATE;
	wfx.wBitsPerSample = 16;
	wfx.nBlockAlign = (wfx.nChannels * wfx.wBitsPerSample) / 8;
	wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;

	HRESULT hr = DirectSoundCreate(NULL, &m_lpds, NULL);

	if (hr != DS_OK)
	{
		return false;
	}

	// Change DirectSound co-op level
	IDirectSound_SetCooperativeLevel(m_lpds, m_hHandle, DSSCL_PRIORITY);

	// Obtain Primary Buffer
	memset(&dsbdesc, 0, sizeof(DSBUFFERDESC));
	dsbdesc.dwSize = sizeof(DSBUFFERDESC);
	dsbdesc.dwFlags = DSBCAPS_PRIMARYBUFFER | DSBCAPS_GLOBALFOCUS;
	dsbdesc.lpwfxFormat = (LPWAVEFORMATEX) &wfx;
	hr = m_lpds->CreateSoundBuffer(&dsbdesc, &m_lpdsbPrimary, NULL);

	// Failed to create primary buffer
	if (m_lpdsbPrimary == NULL)
	{
		// This is not automatically a cause for failure - only
		// older cards typically allow or indeed require a primary
		// buffer - we will allow this to fail gracefully
	}

	for (int i = 0; i < 4; i++)
	{
		AppCreateBasicBuffer(&m_lpdsbChannels[i], wfx, BUFFLEN);
		m_lpdsbChannels[i]->SetPan((i & 1) ? 1000 : -1000);
		m_lpdsbChannels[i]->Play(0, 0, DSBPLAY_LOOPING);
	}

	return true;
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::StartChannel
//
//  PURPOSE:	Start a single audio channel playing back samples
//
//  INPUTS:		BYTE                 Channel number (0-based) to start
//
//  OUTPUT:		void
//
///////////////////////////////////////////////////////////////////////
void __fastcall TOSCAAudioSystem::StartChannel(BYTE channel)
{
	if (ControlRegs[channel].Playing)
	{
		// Perhaps if a channel is already playing it shouldn't restart,
		// but this isn't really compatible with the looping technique used, so
		// the playback is checked separately when the play register is updated
//		return;
	}

	// Convert the V6Z80P formatted location/length registers to actual values
	// compatible with the DirectX playback routine
	ControlRegs[channel].RealLocation = 0x20000 + (ControlRegs[channel].Location << 1);
	this->fOutputPos[channel] = ControlRegs[channel].RealLocation;
	ControlRegs[channel].RealLength = (ControlRegs[channel].Length << 1);

	// Reset default looping/playing flags
	ControlRegs[channel].Looping = true;
	ControlRegs[channel].Playing = true;
	LoopFlags |= (1 << channel);
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::UpdateChannel
//
//  PURPOSE:	Called frequently to update the DirectSound buffers
//
//  INPUTS:		BYTE                 Channel number (0-based)
//					short *					Pointer to audio buffer for filling
//					unsigned int			Bytes required to fill buffer
//					unsigned int			Number of CPU cycles elapsed over
//												this fill period
//
//  OUTPUT:		void
//
///////////////////////////////////////////////////////////////////////
void __fastcall TOSCAAudioSystem::UpdateChannel(BYTE channel, short *buffer,
	unsigned int bytes, unsigned int cycles)
{
	char buf[100];
	DWORD byteOffset = fOutputPos[channel];

	// Iterate through the bytes required to fill the buffer
	while (bytes)
	{
		// Only fill with non-zero data if the channel is playing
		if (ControlRegs[channel].Playing)
		{
			short level = (short)((char)(m_pOwner->MainRAM[byteOffset]))  * (short)ControlRegs[channel].Volume * 2;
			*(buffer++) = level;
			*(buffer++) = level;

			// Check internal prescaler to see if it's time to
			// advance to the next byte in the V6Z80's sample memory
			fSamplePrescaler[channel] -= 1.0f;
			if (fSamplePrescaler[channel] < 1.0f)
			{
				// Obtain Next sample
				fSamplePrescaler[channel] += (double)48000 / (double)ControlRegs[channel].RealSampRate;
				fOutputPos[channel]++;

				// Check for sample looping
				if (fOutputPos[channel] >= (ControlRegs[channel].RealLocation + ControlRegs[channel].RealLength))
				{
					// Channel has looped

					// Set Audio looped IRQ flag
					LoopFlags |= (1 << (4 + channel));

					if (ControlRegs[channel].Looping)
					{
						StartChannel(channel);
					}
					else
					{
						// Clear playing flag
						ControlRegs[channel].Playing = false;
						fOutputPos[channel] = 0x20000 + (ControlRegs[channel].Location << 1);
						LoopFlags &= ~(1 << channel);
					}
				}
				byteOffset = fOutputPos[channel];
			}
		}
		else
		{
			// Channel not playing - fill with the sound of silence
			*(buffer++) = 0;
			*(buffer++) = 0;
		}

		bytes -= 2;
	}
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::UpdateSound
//
//  PURPOSE:	Called frequently by the CPU core to update the
// 				emulated audio hardware
//
//  INPUTS:		unsigned int			Number of CPU cycles elapsed since
//												the last audio update
//
//  OUTPUT:		void
//
///////////////////////////////////////////////////////////////////////
void __fastcall TOSCAAudioSystem::UpdateSound(unsigned int cycles)
{
	DWORD	playPos, unusedWriteCursor;
	DWORD	writeLen;
	LPVOID p1, p2;
	DWORD	l1, l2, ll;
	HRESULT hRes;

	// Iterate through each of the 4 channels
	for (int i = 0; i < 4; i++)
	{
		// Obtain current playhead position
		hRes = m_lpdsbChannels[i]->GetCurrentPosition(&playPos, &unusedWriteCursor);
		if (hRes != DS_OK)
		{
			playPos = 0;
		}

		// Work out start/end addresses
		if (m_nWritePos[i] < playPos)
		{
			writeLen = playPos - m_nWritePos[i];
		}
		else
		{
			writeLen = BUFFLEN - (m_nWritePos[i] - playPos);
		}

		// Lock the playback buffer (and restore the buffers if needed). Also
		// obtains offset positions for the buffer fill (2 of them since we may have
		// circled back around during fill to the start of the buffer)
		while (DS_OK != m_lpdsbChannels[i]->Lock(m_nWritePos[i], writeLen, &p1, &l1, &p2, &l2, 0))
		{
			m_lpdsbChannels[i]->Restore();
			m_lpdsbChannels[i]->Play(0, 0, DSBPLAY_LOOPING);
		}

		// Calculate total length
		ll = l1 + l2;

		// Perform the first fill (if necessary)
		if ((p1) && (l1 > 0))
		{
			UpdateChannel(i, (short *)p1, l1 / 2, (l1 * cycles) / ll);
		}

		// Perform the second fill (if necessary)
		if ((p2) && (l2 > 0))
		{
			UpdateChannel(i, (short *)p2, l2 / 2, (l2 * cycles) / ll);
		}

		// Unlock the playback buffer and update the write position
		m_lpdsbChannels[i]->Unlock(p1, l1, p2, l2);
		m_nWritePos[i] += writeLen;

		// Handle the write position circling back around
		if (m_nWritePos[i] >= BUFFLEN)
		{
			m_nWritePos[i] -= BUFFLEN;
		}
	}
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::GetAudioFlags
//
//  PURPOSE:	Get the flags register to be reported to the V6Z80P
//
//  INPUTS:		void
//
//  OUTPUT:		BYTE						Flags register (see bits below)
//												chan: 	43214321
//												means:   llllpppp
//												l = looped, p = playing
//
///////////////////////////////////////////////////////////////////////
BYTE __fastcall TOSCAAudioSystem::GetAudioFlags(void)
{
	return LoopFlags;
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::ClearLoopFlag
//
//  PURPOSE:	Clear the channel looped flag for specified channel
//
//  INPUTS:		BYTE						Channel number (0-based)
//
//  OUTPUT:		void
//
///////////////////////////////////////////////////////////////////////
void __fastcall TOSCAAudioSystem::ClearLoopFlag(BYTE channel)
{
	if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
	{
		DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
			L"Clear Audio Loop on Channel: %d",
			OPENARRAY(TVarRec, (channel))
		));
	}

	LoopFlags &= ~(1 << (4 + channel));
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::SetPlaybackFlags
//
//  PURPOSE:	Change the playback flags register (which will cause
//					channels to start and/or stop)
//
//  INPUTS:		BYTE						Flags value (only LS-Nybble is
//												relevant, for chans 4321 resp.)
//
//  OUTPUT:		void
//
///////////////////////////////////////////////////////////////////////
void __fastcall TOSCAAudioSystem::SetPlaybackFlags(BYTE flags)
{
	for (int i = 0; i < 4; i++)
	{
		if (flags & (1 << i))
		{
			if (!ControlRegs[i].Playing)
			{
				if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
				{
					DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
						L"Start Channel: %d",
						OPENARRAY(TVarRec, (i))
					));
				}

				StartChannel(i);
			}
		}
		else
		{
				if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
				{
					DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
						L"Stop Channel: %d",
						OPENARRAY(TVarRec, (i))
					));
				}

			ControlRegs[i].Playing = false;
		}
	}
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::WriteAudioPort
//
//  PURPOSE:	Handle a write to the audio registers by the emulated
// 				CPU core
//
//  INPUTS:		BYTE						Address to be written
//					WORD						Value to be written
//
//  OUTPUT:		void
//
///////////////////////////////////////////////////////////////////////
void __fastcall TOSCAAudioSystem::WriteAudioPort(BYTE addr, WORD value)
{
	BYTE chan = (addr & 0x0c) >> 2;
	BYTE cntrl = (addr & 0x03);

	switch (cntrl)
	{
		case 0:	// Location (words from start of sample memory)
			if (ControlRegs[chan].Location != value)
			{
				ControlRegs[chan].Location = value;
				if (ControlRegs[chan].Playing)
				{
					//ControlRegs[chan].Looping = false;
				}

				if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
				{
					DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
						L"Channel %d Location set to %04x (%08x)",
						OPENARRAY(TVarRec, (chan, ControlRegs[chan].Location))
					));
				}
			}
			break;
		case 1:	// Length (in words)
			if (ControlRegs[chan].Length != value)
			{
				ControlRegs[chan].Length = value;
				if (ControlRegs[chan].Playing)
				{
					//ControlRegs[chan].Looping = false;
				}

				if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
				{
					DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
						L"Channel %d Duration set to %04x",
						OPENARRAY(TVarRec, (chan, ControlRegs[chan].Length))
					));
				}
			}
			break;
		case 2:	// Rate (system clocks between samples)
			if (ControlRegs[chan].SampleRate != value)
			{
				ControlRegs[chan].SampleRate = value;

				if (value < 512)
				{
					ControlRegs[chan].RealSampRate = 31250;
				}
				else
				{
					ControlRegs[chan].RealSampRate = 16000000 / value;
				}
				fSamplePrescaler[chan] = (double)48000 / (double)ControlRegs[chan].RealSampRate;

				if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
				{
					DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
						L"Channel %d SampRate set to %04x (%d Hz)",
						OPENARRAY(TVarRec, (chan,
						ControlRegs[chan].SampleRate, ControlRegs[chan].RealSampRate))
					));
				}
			}
			break;
		case 3:	// Volume
			if (ControlRegs[chan].Volume != (value & 0xff))
			{
				ControlRegs[chan].Volume = (value & 0xff);
				if (ControlRegs[chan].Volume > 0x40)
				{
					ControlRegs[chan].Volume = 0x40;
				}

				if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
				{
					DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
						L"Channel %d Volume set to %02x",
						OPENARRAY(TVarRec, (chan, value & 0xff))
					));
				}
			}
			break;
	}
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::ReadAudioPort
//
//  PURPOSE:	Handle a read of the audio registers by the emulated
// 				CPU core
//
//  INPUTS:		BYTE						Address to be read
//
//  OUTPUT:		WORD						Value read
//
///////////////////////////////////////////////////////////////////////
WORD __fastcall TOSCAAudioSystem::ReadAudioPort(BYTE addr)
{
	BYTE chan = (addr & 0x0c) >> 2;
	BYTE cntrl = (addr & 0x03);
	WORD retv = 0;

	switch (cntrl)
	{
		case 0:	// Location (words from start of sample memory)
			retv = ControlRegs[chan].Location;

			if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
			{
				DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
					L"Channel %d Location read",
					OPENARRAY(TVarRec, (chan))
				));
			}
			break;
		case 1:	// Length (in words)
			retv = ControlRegs[chan].Length;

			if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
			{
				DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
					L"Channel %d Duration read",
					OPENARRAY(TVarRec, (chan))
				));
			}
			break;
		case 2:	// Rate (system clocks between samples)
			retv = ControlRegs[chan].SampleRate;

			if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
			{
				DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
					L"Channel %d SampRate read",
					OPENARRAY(TVarRec, (chan))
				));
			}
			break;
		case 3:	// Volume
			retv = ControlRegs[chan].Volume;

			if ((DiagForm) && (DiagForm->IsActive(DIAG_AUDIO)))
			{
				DiagForm->AddMessage(DIAG_AUDIO, UnicodeString::Format(
					L"Channel %d Volume read",
					OPENARRAY(TVarRec, (chan))
				));
			}
			break;
	}

	return retv;
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::TOSCAAudioSystem [CONSTRUCTOR]
//
//  PURPOSE:	Constructor for the Audio System
//
//  INPUTS:		HWND                 Main window's handle
//					TOSCABoard *			Pointer to board class owning this
//												class (required for accessing sample
//												memory)
//
//	 OUTPUT:		none
//
///////////////////////////////////////////////////////////////////////
__fastcall TOSCAAudioSystem::TOSCAAudioSystem(HWND handle, TOSCABoard *owner) :
	m_hHandle(handle), m_pOwner(owner)
{
	for (int i = 0; i < 4; i++)
	{
		ControlRegs[i].Location			= 0;
		ControlRegs[i].Volume			= 0;
		ControlRegs[i].Playing			= false;
		ControlRegs[i].Looping			= true;
		ControlRegs[i].Length			= 0;
		ControlRegs[i].SampleRate		= 0;
		ControlRegs[i].RealLength		= 0;
		ControlRegs[i].RealLocation	= 0x20000;
		ControlRegs[i].RealSampRate	= 31250;
		LoopFlags							= 0x00;
		fSamplePrescaler[i] 				= 1.0f;
		m_lpdsbChannels[i]				= NULL;
		m_nWritePos[i]						= 0;
		fOutputPos[i]						= 0x20000;
	}

	InitSound();
}

///////////////////////////////////////////////////////////////////////
//
//  FUNCTION:	TOSCAAudioSystem::~TOSCAAudioSystem [DESTRUCTOR]
//
//  PURPOSE:	Destructor for the Audio System
//
//  INPUTS:		none
//
//	 OUTPUT:		none
//
///////////////////////////////////////////////////////////////////////
__fastcall TOSCAAudioSystem::~TOSCAAudioSystem()
{
	// Free up primary buffer (if there is one!)

	// NOTE: For some reason, this likes to crash ?!? Must be some
	// DirectX Dumbfuckery since _we_ created the dsb, so surely should
	// release it once finished. Grrrr. Anyway, releasing ds later should
	// also do the job, so no major harm done!

//	if (m_lpdsbPrimary != NULL)
//	{
//		m_lpdsbPrimary->Release();
//		m_lpdsbPrimary = NULL;
//	}

	// Release Direct Sound
	if (m_lpds != NULL)
	{
		m_lpds->Release();
		m_lpds = NULL;
	}
}

//---------------------------------------------------------------------------

#pragma package(smart_init)

