/*
**	PlaySound - Double-buffered IFF-8SVX player
**
**	© Copyright 1992 by Olaf `Olsen' Barthel
**		All Rights Reserved
*/

	/* System includes. */

#include <libraries/iffparse.h>
#include <workbench/startup.h>
#include <graphics/gfxbase.h>
#include <exec/execbase.h>
#include <dos/dosextens.h>
#include <devices/audio.h>
#include <exec/memory.h>

	/* Prototypes. */

#include <clib/iffparse_protos.h>
#include <clib/exec_protos.h>
#include <clib/alib_protos.h>
#include <clib/dos_protos.h>

	/* `VHDR' header format. */

struct Voice8Header
{
	ULONG	oneShotHiSamples,	/* # samples in the high octave 1-shot part */
		repeatHiSamples,	/* # samples in the high octave repeat part */
		samplesPerHiCycle;	/* # samples/cycle in high octave, else 0 */
	UWORD	samplesPerSec;		/* data sampling rate */
	UBYTE	ctOctave,		/* # of octaves of waveforms */
		sCompression;		/* data compression technique used */
	LONG	volume;			/* playback nominal volume from 0 to Unity
					 * (full volume). Map this value into
					 * the output hardware's dynamic range.
					 */
};

	/* Audio channel indexes, including control channel. */

enum	{	AUDIO_LEFT, AUDIO_RIGHT, AUDIO_CONTROL, AUDIO_COUNT };

	/* If run from Shell, we accept two parameters:
	 *
	 *	Pattern		= The files to play
	 *	BufferSize	= The replay buffer size to use.
	 */

enum	{	ARG_PATTERN, ARG_BUFFERSIZE, ARG_COUNT };

	/* Channel allocation bits. */

#define LEFT0F  1
#define RIGHT0F  2
#define RIGHT1F  4
#define LEFT1F  8

	/* Channel allocation masks (to separate the left from
	 * the right).
	 */

#define LEFT_MASK	(LEFT0F | LEFT1F)
#define RIGHT_MASK	(RIGHT0F | RIGHT1F)

	/* Program version identifier. */

STATIC UBYTE *Version = "\0$VER: PlaySound 1.1 (27.4.92)";

	/* Shared libraries. */

struct ExecBase		*SysBase;
struct DosLibrary	*DOSBase;
struct GfxBase		*GfxBase;
struct Library		*IFFParseBase;

	/* Channel allocation scheme is as follows:
	 *
	 *	1) Allocate two stereo channels
	 *	2) Allocate any left channel
	 *	3) Allocate any right channel
	 */

UBYTE AnyChannel[] =
{
	LEFT0F | RIGHT0F,
	LEFT0F | RIGHT1F,
	LEFT1F | RIGHT0F,
	LEFT1F | RIGHT1F,

	LEFT0F,	LEFT1F,
	RIGHT0F,LEFT1F
};

	/* Prototypes for this module. */

LONG __saveds	Main(VOID);
VOID __regargs	StartSound(struct IOAudio **Audio,LONG Rate,LONG Volume,APTR Data,LONG Length,BYTE Stereo);
VOID __regargs	WaitSound(struct IOAudio **Audio,BYTE Stereo);
VOID __regargs	StopSound(struct IOAudio **Audio,BYTE Stereo);
VOID __regargs	HandleSound(STRPTR Name,struct IOAudio **Audio,APTR *AudioData,LONG BufferSize,BYTE Stereo);
BYTE __regargs	PlayIt(STRPTR Name,LONG BufferSize);


	/* Main():
	 *
	 *	The program entry point.
	 */

LONG __saveds
Main()
{
	struct WBStartup	*WBenchMsg;
	LONG			 Result = RETURN_FAIL;
	struct Process		*ThisProcess;
	LONG			 Pri;
	LONG			 BufferSize = 5000;

		/* Set up ExecBase */

	SysBase = *(struct ExecBase **)4;

		/* Pick up current process identifier. */

	ThisProcess = (struct Process *)SysBase -> ThisTask;

		/* If no CLI info is present, wait for Workbench
		 * startup message.
		 */

	if(!ThisProcess -> pr_CLI)
	{
		WaitPort(&ThisProcess -> pr_MsgPort);

		WBenchMsg = (struct WBStartup *)GetMsg(&ThisProcess -> pr_MsgPort);
	}
	else
		WBenchMsg = NULL;

		/* Determine current priority. */

	Pri = ThisProcess -> pr_Task . tc_Node . ln_Pri;

		/* If lower than 20, set the current process priority to 20. */

	if(Pri < 20)
		SetTaskPri(ThisProcess,20);

		/* Open dos.library. */

	if(DOSBase = (struct DosLibrary *)OpenLibrary("dos.library",37))
	{
			/* Open graphics.library */

		if(GfxBase = (struct GfxBase *)OpenLibrary("graphics.library",37))
		{
				/* Open iffparse.library. */

			if(IFFParseBase = OpenLibrary("iffparse.library",37))
			{
					/* If started from Workbench, play the
					 * sound files passed to us via project
					 * icons.
					 */

				if(WBenchMsg)
				{
					LONG i;

						/* Run down the list... */

					for(i = 1 ; i < WBenchMsg -> sm_NumArgs ; i++)
					{
							/* If a lock and a name is present, change
							 * to the project's home directory and try
							 * to play the sound file.
							 */

						if(WBenchMsg -> sm_ArgList[i] . wa_Lock && WBenchMsg -> sm_ArgList[i] . wa_Name)
						{
							CurrentDir(WBenchMsg -> sm_ArgList[i] . wa_Lock);

							PlayIt(WBenchMsg -> sm_ArgList[i] . wa_Name,BufferSize);
						}
					}
				}
				else
				{
					struct AnchorPath *Anchor;

						/* Allocate memory for pattern matching buffer. */

					if(Anchor = (struct AnchorPath *)AllocVec(sizeof(struct AnchorPath) + 512,MEMF_ANY | MEMF_CLEAR))
					{
						STRPTR		 Args[ARG_COUNT] = { NULL, NULL };
						struct RDArgs	*ArgsPtr;

							/* The scanning process is to
							 * be aborted by pressing ^C.
							 */

						Anchor -> ap_BreakBits	= SIGBREAKF_CTRL_C;
						Anchor -> ap_Strlen	= 512;

							/* Read the arguments... */

						if(ArgsPtr = ReadArgs("Pattern,Buffer/K/N",(LONG *)Args,NULL))
						{
							LONG Error = 0;

								/* Did we get a pattern to look for? */

							if(Args[ARG_PATTERN])
							{
								BYTE MatchMade = FALSE;

								Result = RETURN_OK;

									/* Are we to use a special replay
									 * buffer size?
									 */

								if(Args[ARG_BUFFERSIZE])
								{
									BufferSize = *(LONG *)Args[ARG_BUFFERSIZE];

										/* 1000 bytes is the minimum. */

									if(BufferSize < 1000)
										BufferSize = 1000;
								}

									/* Look for the first file/directory to match the pattern. */

								if(!MatchFirst(Args[ARG_PATTERN],Anchor))
								{
									STRPTR Name = Anchor -> ap_Buf;

									for(;;)
									{
											/* Did we find a file or a directory? */

										if(Anchor -> ap_Info . fib_DirEntryType < 0)
										{
											MatchMade = TRUE;

												/* Play the sound file. */

											if(!PlayIt(Name,BufferSize))
											{
												Printf("PlaySound: Error during channel allocation!\n");

												Result = RETURN_ERROR;

												break;
											}

												/* Check for abort signal. */

											if(SetSignal(0,0) & SIGBREAKF_CTRL_C)
											{
												SetSignal(0,SIGBREAKF_CTRL_C);

												PrintFault(ERROR_BREAK,"PlaySound");

												Result = RETURN_WARN;

												break;
											}
										}

											/* Scan for the next matching file name. */

										if(MatchNext(Anchor))
										{
											Error = IoErr();

											if(Error && Error != ERROR_NO_MORE_ENTRIES)
											{
												PrintFault(Error,"PlaySound");

												if(Error == ERROR_BREAK)
													Result = RETURN_WARN;
												else
													Result = RETURN_ERROR;
											}

											break;
										}
									}

										/* Free pattern matching auxilary data. */

									MatchEnd(Anchor);
								}
								else
								{
									Error = IoErr();

									if(Error && Error != ERROR_NO_MORE_ENTRIES)
									{
										PrintFault(Error,"PlaySound");

										if(Error == ERROR_BREAK)
											Result = RETURN_WARN;
										else
											Result = RETURN_ERROR;
									}
								}

									/* Did we find any matching files? */

								if(!MatchMade && !Error)
								{
									Printf("PlaySound: No matching files were found.\n");

									Result = RETURN_WARN;
								}
							}
							else
								PrintFault(ERROR_REQUIRED_ARG_MISSING,"PlaySound");

								/* Free argument parser data. */

							FreeArgs(ArgsPtr);
						}

							/* Free pattern matching buffer. */

						FreeVec(Anchor);
					}
				}

					/* Clean up... */

				CloseLibrary(IFFParseBase);
			}
			else
			{
				if(ThisProcess -> pr_CLI)
					Printf("PlaySound: Failed to open iffparse.library!\a\n");
			}

			CloseLibrary(GfxBase);
		}
		else
		{
			if(ThisProcess -> pr_CLI)
				Printf("PlaySound: Failed to open graphics.library!\a\n");
		}

		CloseLibrary(DOSBase);
	}

		/* If started from Workbench, reply the startup message. */

	if(WBenchMsg)
	{
		Forbid();

		ReplyMsg(&WBenchMsg -> sm_Message);
	}

		/* Reset the task priority. */

	SetTaskPri(ThisProcess,Pri);

		/* Return success. */

	return(Result);
}

	/* StartSound():
	 *
	 *	Play a sound, play it in stereo if possible.
	 */

VOID __regargs
StartSound(struct IOAudio **Audio,LONG Rate,LONG Volume,APTR Data,LONG Length,BYTE Stereo)
{
		/* Set up the left channel. */

	Audio[AUDIO_LEFT] -> ioa_Request . io_Command	= CMD_WRITE;
	Audio[AUDIO_LEFT] -> ioa_Request . io_Flags	= ADIOF_PERVOL;
	Audio[AUDIO_LEFT] -> ioa_Period			= Rate;
	Audio[AUDIO_LEFT] -> ioa_Volume			= Volume;
	Audio[AUDIO_LEFT] -> ioa_Cycles			= 1;
	Audio[AUDIO_LEFT] -> ioa_Data			= Data;
	Audio[AUDIO_LEFT] -> ioa_Length			= Length;

		/* Are we to play the sound in stereo? */

	if(Stereo)
	{
			/* To avoid echoes or delays, we will synchronize
			 * both channels. To do the trick, we stop audio
			 * output until both audio IO requests are running
			 * and start them both at the same time using CMD_START.
			 */

		Audio[AUDIO_CONTROL] -> ioa_Request . io_Command = CMD_STOP;

		DoIO(Audio[AUDIO_CONTROL]);

			/* Set up the right channel. */

		Audio[AUDIO_RIGHT] -> ioa_Request . io_Command	= CMD_WRITE;
		Audio[AUDIO_RIGHT] -> ioa_Request . io_Flags	= ADIOF_PERVOL;
		Audio[AUDIO_RIGHT] -> ioa_Period		= Rate;
		Audio[AUDIO_RIGHT] -> ioa_Volume		= Volume;
		Audio[AUDIO_RIGHT] -> ioa_Cycles		= 1;
		Audio[AUDIO_RIGHT] -> ioa_Data			= Data;
		Audio[AUDIO_RIGHT] -> ioa_Length		= Length;

			/* Start both audio IO requests. */

		BeginIO(Audio[AUDIO_LEFT]);
		BeginIO(Audio[AUDIO_RIGHT]);

			/* Cause audio.device to play the sound on two
			 * stereo channels.
			 */

		Audio[AUDIO_CONTROL] -> ioa_Request . io_Command = CMD_START;

		DoIO(Audio[AUDIO_CONTROL]);
	}
	else
		BeginIO(Audio[AUDIO_LEFT]);
}

	/* WaitSound(struct IOAudio **Audio,BYTE Stereo):
	 *
	 *	Wait for sound to stop.
	 */

VOID __regargs
WaitSound(struct IOAudio **Audio,BYTE Stereo)
{
	WaitIO(Audio[AUDIO_LEFT]);

	if(Stereo)
		WaitIO(Audio[AUDIO_RIGHT]);
}

	/* StopSound(struct IOAudio **Audio,BYTE Stereo):
	 *
	 *	Abort any sound currently playing.
	 */

VOID __regargs
StopSound(struct IOAudio **Audio,BYTE Stereo)
{
	if(!CheckIO(Audio[AUDIO_LEFT]))
		AbortIO(Audio[AUDIO_LEFT]);

	if(Stereo)
	{
		if(!CheckIO(Audio[AUDIO_RIGHT]))
			AbortIO(Audio[AUDIO_RIGHT]);

		WaitIO(Audio[AUDIO_LEFT]);
		WaitIO(Audio[AUDIO_RIGHT]);
	}
	else
		WaitIO(Audio[AUDIO_LEFT]);
}

	/* HandleSound():
	 *
	 *	Play a sound using double-buffering.
	 */

VOID __regargs
HandleSound(STRPTR Name,struct IOAudio **Audio,APTR *AudioData,LONG BufferSize,BYTE Stereo)
{
	struct IFFHandle	*Handle;
	struct StoredProperty	*Prop;
	struct Voice8Header	*VoiceHeader;

	LONG			 Rate,
				 Length,
				 Volume;

		/* Allocate an IFF handle. */

	if(Handle = AllocIFF())
	{
			/* Open the sound file for reading. */

		if(Handle -> iff_Stream = Open(Name,MODE_OLDFILE))
		{
				/* Say it's a plain AmigaDOS file that we
				 * are about to read.
				 */

			InitIFFasDOS(Handle);

				/* Open the file for reading. */

			if(!OpenIFF(Handle,IFFF_READ))
			{
					/* Remember the voice header chunk if encountered. */

				if(!PropChunk(Handle,'8SVX','VHDR'))
				{
						/* Stop in front of the data body chunk. */

					if(!StopChunk(Handle,'8SVX','BODY'))
					{
							/* Scan the file... */

						if(!ParseIFF(Handle,IFFPARSE_SCAN))
						{
								/* Try to find the voice header chunk. */

							if(Prop = FindProp(Handle,'8SVX','VHDR'))
							{
								VoiceHeader = (struct Voice8Header *)Prop -> sp_Data;

									/* No compression and only a single octave, please! */

								if(!VoiceHeader -> sCompression && VoiceHeader -> ctOctave == 1)
								{
									struct ContextNode *ContextNode;

										/* Get information on the current chunk. */

									if(ContextNode = CurrentChunk(Handle))
									{
										LONG	Bytes	= 0;
										BYTE	Played	= FALSE,
											Buffer	= 0;

											/* Play either the one-shot or the repeat part,
											 * but not both.
											 */

										Length	= VoiceHeader -> oneShotHiSamples ? VoiceHeader -> oneShotHiSamples : VoiceHeader -> repeatHiSamples;

											/* Determine replay rate. */

										Rate	= (GfxBase -> DisplayFlags & PAL ? 3546895 : 3579545) / VoiceHeader -> samplesPerSec;

											/* Determine replay volume. */

										Volume	= (VoiceHeader -> volume * 64) / 0x10000;

											/* Start playing... */

										while(Length)
										{
												/* Are we to abort the playing process? */

											if(SetSignal(0,0) & SIGBREAKF_CTRL_C)
											{
												if(Played)
												{
													StopSound(Audio,Stereo);

													Played = FALSE;
												}

												break;
											}

												/* Are we to abort the current sound? */

											if(SetSignal(0,0) & SIGBREAKF_CTRL_D)
											{
												SetSignal(0,SIGBREAKF_CTRL_D);

												if(Played)
												{
													StopSound(Audio,Stereo);

													Played = FALSE;
												}

												break;
											}

												/* Determine number of bytes to read. */

											if(Length < BufferSize)
												Bytes = Length;
											else
												Bytes = BufferSize;

											Length -= Bytes;

												/* Read the following data. */

											if(ReadChunkBytes(Handle,AudioData[Buffer],Bytes) != Bytes)
												break;
											else
											{
													/* If still playing, wait until
													 * the previous sound has finished.
													 */

												if(Played)
													WaitSound(Audio,Stereo);

												StartSound(Audio,Rate,Volume,AudioData[Buffer],Bytes,Stereo);

													/* Remember that we have been active. */

												Played = TRUE;

													/* Toggle the buffer to play/read. */

												Buffer ^= 1;
											}
										}

											/* Wait until the sound is finished. */

										if(Played)
											WaitSound(Audio,Stereo);
									}
								}
							}
						}
					}
				}

					/* Close the IFF read handle. */

				CloseIFF(Handle);
			}

				/* Close the file. */

			Close(Handle -> iff_Stream);
		}

			/* Free the handle data. */

		FreeIFF(Handle);
	}
}

	/* PlayIt(STRPTR Name,LONG BufferSize):
	 *
	 *	Do the setups necessary to play a sound.
	 */

BYTE __regargs
PlayIt(STRPTR Name,LONG BufferSize)
{
	BYTE *AudioData[2],Result = FALSE;

		/* Allocate the audio buffer. */

	if(AudioData[0] = AllocVec(BufferSize * 2,MEMF_CHIP))
	{
		struct MsgPort *AudioPort;

			/* Split the audio buffer in two. */

		AudioData[1] = AudioData[0] + BufferSize;

			/* Create the IO request reply port. */

		if(AudioPort = CreateMsgPort())
		{
			struct IOAudio *Audio[AUDIO_COUNT];

				/* Allocate the audio IO requests. */

			if(Audio[AUDIO_LEFT] = (struct IOAudio *)AllocVec(sizeof(struct IOAudio) * AUDIO_COUNT,MEMF_PUBLIC | MEMF_CLEAR))
			{
					/* Cut the pie into three pieces. */

				Audio[AUDIO_RIGHT]	= Audio[AUDIO_LEFT] + 1;
				Audio[AUDIO_CONTROL]	= Audio[AUDIO_RIGHT] + 1;

					/* Open audio.device, allocate the channels on the fly. */

				Audio[AUDIO_LEFT] -> ioa_Request . io_Message . mn_Node . ln_Type	= NT_MESSAGE;
				Audio[AUDIO_LEFT] -> ioa_Request . io_Message . mn_Length		= sizeof(struct IOAudio);
				Audio[AUDIO_LEFT] -> ioa_Request . io_Command				= ADCMD_ALLOCATE;
				Audio[AUDIO_LEFT] -> ioa_Request . io_Flags				= ADIOF_NOWAIT;
				Audio[AUDIO_LEFT] -> ioa_Data						= AnyChannel;
				Audio[AUDIO_LEFT] -> ioa_Length						= sizeof(AnyChannel);
				Audio[AUDIO_LEFT] -> ioa_Request . io_Message . mn_ReplyPort		= AudioPort;

				if(!OpenDevice(AUDIONAME,0,Audio[AUDIO_LEFT],0))
				{
					WORD Count,i;

						/* Check whether we got just a single
						 * or two separate stereo channels.
						 */

					for(i = 0 ; i < sizeof(AnyChannel) ; i++)
					{
						if((((LONG)Audio[AUDIO_LEFT] -> ioa_Request . io_Unit) & AnyChannel[i]) == AnyChannel[i])
						{
							Count = i;

							break;
						}
					}

						/* Set up the three audio IO requests. */

					for(i = AUDIO_RIGHT ; i < AUDIO_COUNT ; i++)
						CopyMem(Audio[AUDIO_LEFT],Audio[i],sizeof(struct IOAudio));

						/* We have been successful so far. */

					Result = TRUE;

						/* Did we get two stereo channels? */

					if(Count < 4)
					{
							/* Retain just the left channel. */

						Audio[AUDIO_LEFT] -> ioa_Request . io_Unit	= (APTR)(((ULONG)Audio[AUDIO_LEFT] -> ioa_Request . io_Unit) & LEFT_MASK);

							/* Retain just the right channel. */

						Audio[AUDIO_RIGHT] -> ioa_Request . io_Unit	= (APTR)(((ULONG)Audio[AUDIO_RIGHT] -> ioa_Request . io_Unit) & RIGHT_MASK);

							/* Play the sound. */

						HandleSound(Name,Audio,(APTR *)AudioData,BufferSize,TRUE);
					}
					else
						HandleSound(Name,Audio,(APTR *)AudioData,BufferSize,FALSE);

						/* Close the device, freeing the channels. */

					CloseDevice(Audio[AUDIO_CONTROL]);
				}

					/* Delete the audio request block. */

				FreeVec(Audio[AUDIO_LEFT]);
			}

				/* Delete the audio IO request reply port. */

			DeleteMsgPort(AudioPort);
		}

			/* Free the replay buffer. */

		FreeVec(AudioData[0]);
	}

	return(Result);
}
