
from __future__ import with_statement

import threading
import logging
import time
from ctypes import *
from JetUtils import OsWindows


# stream state 
EAS_STATE_READY = 0
EAS_STATE_PLAY = 1
EAS_STATE_STOPPING = 2
EAS_STATE_PAUSING = 3
EAS_STATE_STOPPED = 4
EAS_STATE_PAUSED = 5
EAS_STATE_OPEN = 6
EAS_STATE_ERROR = 7
EAS_STATE_EMPTY = 8

# EAS error codes
EAS_SUCCESS	= 0
EAS_FAILURE = -1
EAS_ERROR_INVALID_MODULE = -2
EAS_ERROR_MALLOC_FAILED = -3
EAS_ERROR_FILE_POS = -4
EAS_ERROR_INVALID_FILE_MODE = -5
EAS_ERROR_FILE_SEEK = -6
EAS_ERROR_FILE_LENGTH = -7
EAS_ERROR_NOT_IMPLEMENTED = -8
EAS_ERROR_CLOSE_FAILED = -9
EAS_ERROR_FILE_OPEN_FAILED = -10
EAS_ERROR_INVALID_HANDLE = -11
EAS_ERROR_NO_MIX_BUFFER = -12
EAS_ERROR_PARAMETER_RANGE = -13
EAS_ERROR_MAX_FILES_OPEN = -14
EAS_ERROR_UNRECOGNIZED_FORMAT = -15
EAS_BUFFER_SIZE_MISMATCH = -16
EAS_ERROR_FILE_FORMAT = -17
EAS_ERROR_SMF_NOT_INITIALIZED = -18
EAS_ERROR_LOCATE_BEYOND_END = -19
EAS_ERROR_INVALID_PCM_TYPE = -20
EAS_ERROR_MAX_PCM_STREAMS = -21
EAS_ERROR_NO_VOICE_ALLOCATED = -22
EAS_ERROR_INVALID_CHANNEL = -23
EAS_ERROR_ALREADY_STOPPED = -24
EAS_ERROR_FILE_READ_FAILED = -25
EAS_ERROR_HANDLE_INTEGRITY = -26
EAS_ERROR_MAX_STREAMS_OPEN = -27
EAS_ERROR_INVALID_PARAMETER = -28
EAS_ERROR_FEATURE_NOT_AVAILABLE = -29
EAS_ERROR_SOUND_LIBRARY = -30
EAS_ERROR_NOT_VALID_IN_THIS_STATE = -31
EAS_ERROR_NO_VIRTUAL_SYNTHESIZER = -32
EAS_ERROR_FILE_ALREADY_OPEN = -33
EAS_ERROR_FILE_ALREADY_CLOSED = -34
EAS_ERROR_INCOMPATIBLE_VERSION = -35
EAS_ERROR_QUEUE_IS_FULL = -36
EAS_ERROR_QUEUE_IS_EMPTY = -37
EAS_ERROR_FEATURE_ALREADY_ACTIVE = -38

# special result codes
EAS_EOF = 3
EAS_STREAM_BUFFERING = 4

# buffer full error returned from Render
EAS_BUFFER_FULL = 5

# file types
file_types = (
	'Unknown',
	'SMF Type 0 (.mid)',
	'SMF Type 1 (.mid)',
	'SMAF - Unknown type (.mmf)',
	'SMAF MA-2 (.mmf)',
	'SMAF MA-3 (.mmf)',
	'SMAF MA-5 (.mmf)',
	'CMX/QualComm  (.pmd)',
	'MFi (NTT/DoCoMo i-mode)',
	'OTA/Nokia (.ott)',
	'iMelody (.imy)',
	'RTX/RTTTL (.rtx)',
	'XMF Type 0 (.xmf)',
	'XMF Type 1 (.xmf)',
	'WAVE/PCM (.wav)',
	'WAVE/IMA-ADPCM (.wav)',
	'MMAPI Tone Control (.js)'
)

stream_states = (
	'Ready',
	'Play',
	'Stopping',
	'Stopped',
	'Pausing',
	'Paused',
	'Open',
	'Error',
	'Empty'
)

# iMode play modes
IMODE_PLAY_ALL = 0
IMODE_PLAY_PARTIAL = 1

# callback type for metadata
EAS_METADATA_CBFUNC = CFUNCTYPE(c_int, c_int, c_char_p, c_ulong)

# callbacks for external audio
EAS_EXT_PRG_CHG_FUNC = CFUNCTYPE(c_int, c_void_p, c_void_p)
EAS_EXT_EVENT_FUNC = CFUNCTYPE(c_int, c_void_p, c_void_p)

# callback for aux mixer decoder
EAS_DECODER_FUNC = CFUNCTYPE(c_void_p, c_void_p, c_int, c_int)

# DLL path
if OsWindows():
	EAS_DLL_PATH = "EASDLL.dll"
else:
	EAS_DLL_PATH = "libEASLIb.dylib"

eas_dll = None

# logger
eas_logger = None

#---------------------------------------------------------------
# InitEASModule
#---------------------------------------------------------------
def InitEASModule (dll_path=None):
	global eas_dll
	global eas_logger

	
	# initialize logger
	if eas_logger is None:
		eas_logger = logging.getLogger('EAS')

	# initialize path to DLL
	if dll_path is None:
		dll_path=EAS_DLL_PATH

	# intialize DLL
	if eas_dll is None:
		eas_dll = cdll.LoadLibrary(dll_path)

#---------------------------------------------------------------
# S_JET_CONFIG
#---------------------------------------------------------------
class S_JET_CONFIG (Structure):
	_fields_ = [('appLowNote', c_ubyte)]

#---------------------------------------------------------------
# S_EXT_AUDIO_PRG_CHG 
#---------------------------------------------------------------
class S_EXT_AUDIO_PRG_CHG (Structure):
	_fields_ = [('bank', c_ushort),
				('program', c_ubyte),
				('channel', c_ubyte)]

#---------------------------------------------------------------
# S_EXT_AUDIO_EVENT 
#---------------------------------------------------------------
class S_EXT_AUDIO_EVENT (Structure):
	_fields_ = [('channel', c_ubyte),
				('note', c_ubyte),
				('velocity', c_ubyte),
				('noteOn', c_ubyte)]

#---------------------------------------------------------------
# S_MIDI_CONTROLLERS 
#---------------------------------------------------------------
class S_MIDI_CONTROLLERS (Structure):
	_fields_ = [('modWheel', c_ubyte),
				('volume', c_ubyte),
				('pan', c_ubyte),
				('expression', c_ubyte),
				('channelPressure', c_ubyte)]

#---------------------------------------------------------------
# WAVEFORMAT 
#---------------------------------------------------------------
class WAVEFORMAT (Structure):
	_fields_ = [('wFormatTag', c_ushort),
				('nChannels', c_ushort),
				('nSamplesPerSec', c_ulong),
				('nAvgBytesPerSec', c_ulong),
				('nBlockAlign', c_ushort),
				('wBitsPerSample', c_ushort)]

#---------------------------------------------------------------
# EAS_Exception 
#---------------------------------------------------------------
class EAS_Exception (Exception):
	def __init__ (self, result_code, msg, function=None):
		self.msg = msg
		self.result_code = result_code
		self.function = function
	def __str__ (self):
		return self.msg

#---------------------------------------------------------------
# Log callback function
#---------------------------------------------------------------
# map EAS severity levels to the Python logging module
severity_mapping = (logging.CRITICAL, logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG)
LOG_FUNC_TYPE = CFUNCTYPE(c_int, c_int, c_char_p)
def Log (level, msg):
	eas_logger.log(severity_mapping[level], msg)
	return level
LogCallback = LOG_FUNC_TYPE(Log)

#---------------------------------------------------------------
# EAS_Stream
#---------------------------------------------------------------
class EAS_Stream (object):
	def __init__ (self, handle, eas):
		eas_logger.debug('EAS_Stream.__init__')
		self.handle = handle
		self.eas = eas

	def SetVolume (self, volume):
		"""Set the stream volume"""
		eas_logger.debug('Call EAS_SetVolume: volume=%d' % volume)
		with self.eas.lock:
			result = eas_dll.EAS_SetVolume(self.eas.handle, self.handle, volume)
			if result:
				raise EAS_Exception(result, 'EAS_SetVolume error %d on file %s' % (result, self.path), 'EAS_SetVolume')

	def GetVolume (self):
		"""Get the stream volume."""
		eas_logger.debug('Call EAS_GetVolume')
		with self.eas.lock:
			volume = eas_dll.EAS_GetVolume(self.eas.handle, self.handle)
			if volume < 0:
				raise EAS_Exception(volume, 'EAS_GetVolume error %d on file %s' % (volume, self.path), 'EAS_GetVolume')
		eas_logger.debug('EAS_GetVolume: volume=%d' % volume)
		return volume

	def SetPriority (self, priority):
		"""Set the stream priority"""
		eas_logger.debug('Call EAS_SetPriority: priority=%d' % priority)
		with self.eas.lock:
			result = eas_dll.EAS_SetPriority(self.eas.handle, self.handle, priority)
			if result:
				raise EAS_Exception(result, 'EAS_SetPriority error %d on file %s' % (result, self.path), 'EAS_SetPriority')

	def GetPriority (self):
		"""Get the stream priority."""
		eas_logger.debug('Call EAS_GetPriority')
		priority = c_int(0)
		with self.eas.lock:
			result = eas_dll.EAS_GetPriority(self.eas.handle, self.handle, byref(priority))
			if result:
				raise EAS_Exception(result, 'EAS_GetPriority error %d on file %s' % (result, self.path), 'EAS_GetPriority')
		eas_logger.debug('EAS_GetPriority: priority=%d' % priority.value)
		return priority.value

	def SetTransposition (self, transposition):
		"""Set the transposition of a stream."""
		eas_logger.debug('Call EAS_SetTransposition: transposition=%d' % transposition)
		with self.eas.lock:
			result = eas_dll.EAS_SetTransposition(self.eas.handle, self.handle, transposition)
			if result:
				raise EAS_Exception(result, 'EAS_SetTransposition error %d on file %s' % (result, self.path), 'EAS_SetTransposition')

	def SetPolyphony (self, polyphony):
		"""Set the polyphony of a stream."""
		eas_logger.debug('Call EAS_SetPolyphony: polyphony=%d' % polyphony)
		with self.eas.lock:
			result = eas_dll.EAS_SetPolyphony(self.eas.handle, self.handle, polyphony)
			if result:
				raise EAS_Exception(result, 'EAS_SetPolyphony error %d on file %s' % (result, self.path), 'EAS_SetPolyphony')

	def GetPolyphony (self):
		"""Get the polyphony of a stream."""
		eas_logger.debug('Call EAS_GetPolyphony')
		polyphony = c_int(0)
		with self.eas.lock:
			result = eas_dll.EAS_GetPolyphony(self.eas.handle, self.handle, byref(polyphony))
			if result:
				raise EAS_Exception(result, 'EAS_GetPolyphony error %d on file %s' % (result, self.path), 'EAS_GetPolyphony')
		eas_logger.debug('EAS_SetPolyphony: polyphony=%d' % polyphony.value)
		return polyphony.value

	def SelectLib (self, test_lib=False):
		eas_logger.debug('Call EAS_SelectLib: test_lib=%s' % test_lib)
		with self.eas.lock:
			result = eas_dll.EAS_SelectLib(self.eas.handle, self.handle, test_lib)
			if result:
				raise EAS_Exception(result, 'EAS_SelectLib error %d on file %s' % (result, self.path), 'EAS_SelectLib')

	def LoadDLSCollection (self, path):
		eas_logger.debug('Call EAS_LoadDLSCollection: lib_path=%d' % path)
		with self.eas.lock:
			result = eas_dll.EAS_LoadDLSCollection(self.eas.handle, self.handle, path)
			if result:
				raise EAS_Exception(result, 'EAS_LoadDLSCollection error %d on file %s lib %s' % (result, self.path, path), 'EAS_LoadDLSCollection')

	def RegExtAudioCallback (self, user_data, prog_chg_func, event_func):
		"""Register an external audio callback."""
		eas_logger.debug('Call EAS_RegExtAudioCallback')
		if prog_chg_func is not None:
			prog_chg_func = EAS_EXT_PRG_CHG_FUNC(prog_chg_func)
		else:
			prog_chg_func = 0
		if event_func is not None:
			event_func = EAS_EXT_EVENT_FUNC(event_func)
		else:
			event_func = 0
		with self.eas.lock:
			result = eas_dll.EAS_RegExtAudioCallback(self.eas.handle, self.handle, user_data, prog_chg_func, event_func)
			if result:
				raise EAS_Exception(result, 'EAS_RegExtAudioCallback error %d on file %s' % (result, self.path), 'EAS_RegExtAudioCallback')

	def SetPlayMode (self, play_mode):
		"""Set play mode on a stream."""
		eas_logger.debug('Call EAS_SetPlayMode: play_mode=%d' % play_mode)
		with self.eas.lock:
			result = eas_dll.EAS_SetPlayMode(self.eas.handle, self.handle, play_mode)
			if result:
				raise EAS_Exception(result, 'EAS_SetPlayMode error %d on file %s' % (result, self.path), 'EAS_SetPlayMode')

"""
EAS_PUBLIC EAS_RESULT EAS_GetMIDIControllers (EAS_DATA_HANDLE pEASData, EAS_HANDLE streamHandle, EAS_U8 channel, S_MIDI_CONTROLLERS *pControl);
"""

#---------------------------------------------------------------
# EAS_File
#---------------------------------------------------------------
class EAS_File (EAS_Stream):
	def __init__ (self, path, handle, eas):
		EAS_Stream.__init__(self, handle, eas)
		eas_logger.debug('EAS_File.__init__')
		self.path = path
		self.prepared = False

	def Prepare (self):
		"""Prepare an audio file for playback"""
		if self.prepared:
			eas_logger.warning('Prepare already called on file %s' % self.path)
		else:
			with self.eas.lock:
				eas_logger.debug('Call EAS_Prepare for file: %s' % self.path)
				result = eas_dll.EAS_Prepare(self.eas.handle, self.handle)
				if result:
					raise EAS_Exception(result, 'EAS_Prepare error %d on file %s' % (result, self.path), 'EAS_Prepare')
				self.prepared = True

	def State (self):
		"""Get stream state."""
		with self.eas.lock:
			eas_logger.debug('Call EAS_State for file: %s' % self.path)
			state = c_long(-1)
			result = eas_dll.EAS_State(self.eas.handle, self.handle, byref(state))
			if result:
				raise EAS_Exception(result, 'EAS_State error %d on file %s' % (result, self.path), 'EAS_State')
			eas_logger.debug('EAS_State: file=%s, state=%s' % (self.path, stream_states[state.value]))
			return state.value

	def Close (self):
		"""Close audio file."""
		if hasattr(self, 'handle'):
			with self.eas.lock:
				eas_logger.debug('Call EAS_CloseFile for file: %s' % self.path)
				result = eas_dll.EAS_CloseFile(self.eas.handle, self.handle)
				if result:
					raise EAS_Exception(result, 'EAS_CloseFile error %d on file %s' % (result, self.path), 'EAS_CloseFile')

				# remove file from the EAS object
				self.eas.audio_streams.remove(self)
				
			# clean up references
			del self.handle
			del self.eas
			del self.path

	def Pause (self):
		"""Pause a stream."""
		eas_logger.debug('Call EAS_Pause')
		with self.eas.lock:
			result = eas_dll.EAS_Pause(self.eas.handle, self.handle)
			if result:
				raise EAS_Exception(result, 'EAS_Pause error %d on file %s' % (result, self.path), 'EAS_Pause')

	def Resume (self):
		"""Resume a stream."""
		eas_logger.debug('Call EAS_Resume')
		with self.eas.lock:
			result = eas_dll.EAS_Resume(self.eas.handle, self.handle)
			if result:
				raise EAS_Exception(result, 'EAS_Resume error %d on file %s' % (result, self.path), 'EAS_Resume')

	def Locate (self, secs, offset=False):
		"""Set the playback position of a stream in seconds."""
		eas_logger.debug('Call EAS_Locate: location=%.3f, relative=%s' % (secs, offset))
		with self.eas.lock:
			result = eas_dll.EAS_Locate(self.eas.handle, self.handle, int(secs * 1000 + 0.5), offset)
			if result:
				raise EAS_Exception(result, 'EAS_Locate error %d on file %s' % (result, self.path), 'EAS_Locate')

	def GetLocation (self):
		"""Get the stream location in seconds."""
		eas_logger.debug('Call EAS_GetLocation')
		msecs = c_int(0)
		with self.eas.lock:
			result = eas_dll.EAS_GetLocation(self.eas.handle, self.handle, byref(msecs))
			if result:
				raise EAS_Exception(result, 'EAS_GetLocation error %d on file %s' % (result, self.path), 'EAS_GetLocation')
		msecs = float(msecs.value) / 1000 
		eas_logger.debug('EAS_GetLocation: location=%.3f' % msecs)
		return msecs

	def GetFileType (self):
		"""Get the file type."""
		eas_logger.debug('Call EAS_GetFileType')
		file_type = c_int(0)
		with self.eas.lock:
			result = eas_dll.EAS_GetFileType(self.eas.handle, self.handle, byref(file_type))
			if result:
				raise EAS_Exception(result, 'EAS_GetFileType error %d on file %s' % (result, self.path), 'EAS_GetFileType')
		file_type = file_type.value
		if file_type < len(file_types):
			file_desc = file_types[file_type]
		else:
			file_desc = 'Unrecognized type %d' % file_type
		eas_logger.debug('EAS_GetFileType: type=%d, desc=%s' % (file_type, file_desc))
		return (file_type, file_desc)

	def SetRepeat (self, count):
		"""Set the repeat count of a stream."""
		eas_logger.debug('Call EAS_SetRepeat: count=%d' % count)
		with self.eas.lock:
			result = eas_dll.EAS_SetRepeat(self.eas.handle, self.handle, count)
			if result:
				raise EAS_Exception(result, 'EAS_SetRepeat error %d on file %s' % (result, self.path), 'EAS_SetRepeat')

	def GetRepeat (self):
		"""Get the repeat count of a stream."""
		eas_logger.debug('Call EAS_GetRepeat')
		count = c_int(0)
		with self.eas.lock:
			result = eas_dll.EAS_GetRepeat(self.eas.handle, self.handle, byref(count))
			if result:
				raise EAS_Exception(result, 'EAS_GetRepeat error %d on file %s' % (result, self.path), 'EAS_GetRepeat')
		eas_logger.debug('EAS_GetRepeat: count=%d' % count.value)
		return count.value

	def SetPlaybackRate (self, rate):
		"""Set the playback rate of a stream."""
		eas_logger.debug('Call EAS_SetPlaybackRate')
		with self.eas.lock:
			result = eas_dll.EAS_SetPlaybackRate(self.eas.handle, self.handle, rate)
			if result:
				raise EAS_Exception(result, 'EAS_SetPlaybackRate error %d on file %s' % (result, self.path), 'EAS_SetPlaybackRate')

	def ParseMetaData (self):
		"""Parse the metadata in a file."""
		eas_logger.debug('Call EAS_ParseMetaData')
		length = c_int(0)
		with self.eas.lock:
			result = eas_dll.EAS_ParseMetaData(self.eas.handle, self.handle, byref(length))
			if result:
				raise EAS_Exception(result, 'EAS_ParseMetaData error %d on file %s' % (result, self.path), 'EAS_ParseMetaData')
		return float(length.value) / 1000.0

	def RegisterMetaDataCallback (self, func, buf, buf_size, user_data):
		"""Register a metadata callback."""
		eas_logger.debug('Call EAS_RegisterMetaDataCallback')
		with self.eas.lock:
			if func is not None:
				callback = EAS_METADATA_CBFUNC(func)
			else:
				callback = 0
			result = eas_dll.EAS_RegisterMetaDataCallback(self.eas.handle, self.handle, callback, buf, buf_size, user_data)
			if result:
				raise EAS_Exception(result, 'EAS_RegisterMetaDataCallback error %d on file %s' % (result, self.path), 'EAS_RegisterMetaDataCallback')

	def GetWaveFmtChunk (self):
		"""Get the file type."""
		eas_logger.debug('Call EAS_GetWaveFmtChunk')
		wave_fmt_chunk = c_void_p(0)
		with self.eas.lock:
			result = eas_dll.EAS_GetWaveFmtChunk(self.eas.handle, self.handle, byref(wave_fmt_chunk))
			if result:
				raise EAS_Exception(result, 'EAS_GetWaveFmtChunk error %d on file %s' % (result, self.path), 'EAS_GetWaveFmtChunk')
		return cast(wave_fmt_chunk, POINTER(WAVEFORMAT)).contents

	def Play (self, max_time=None):
		"""Plays the file to the end or max_time."""
		eas_logger.debug('EAS_File.Play')
		if not self.prepared:
			self.Prepare()
		if max_time is not None:
			max_time += self.eas.GetRenderTime()
		while self.State() not in (EAS_STATE_STOPPED, EAS_STATE_ERROR, EAS_STATE_EMPTY):
			self.eas.Render()
			if max_time is not None:
				if self.eas.GetRenderTime() >= max_time:
					eas_logger.info('Max render time exceeded - stopping playback')
					self.Pause()
					self.eas.Render()
					break

#---------------------------------------------------------------
# EAS_MIDIStream
#---------------------------------------------------------------
class EAS_MIDIStream (EAS_Stream):
	def Write(self, data):
		"""Write data to MIDI stream."""
		with self.eas.lock:
			result = eas_dll.EAS_WriteMIDIStream(self.eas.handle, self.handle, data, len(data))
			if result:
				raise EAS_Exception(result, 'EAS_WriteMIDIStream error %d' % result, 'EAS_WriteMIDIStream')

	def Close (self):
		"""Close MIDI stream."""
		if hasattr(self, 'handle'):
			with self.eas.lock:
				eas_logger.debug('Call EAS_CloseMIDIStream')
				result = eas_dll.EAS_CloseMIDIStream(self.eas.handle, self.handle)
				if result:
					raise EAS_Exception(result, 'EAS_CloseFile error %d' % result, 'EAS_CloseMIDIStream')

				# remove file from the EAS object
				self.eas.audio_streams.remove(self)
				
			# clean up references
			del self.handle
			del self.eas

#---------------------------------------------------------------
# EAS_Config 
#---------------------------------------------------------------
class EAS_Config (Structure):
	_fields_ = [('libVersion', c_ulong),
				('checkedVersion', c_int),
				('maxVoices', c_long),
				('numChannels', c_long),
				('sampleRate', c_long),
				('mixBufferSize', c_long),
				('filterEnabled', c_int),
				('buildTimeStamp', c_ulong),
				('buildGUID', c_char_p)]

#---------------------------------------------------------------
# EAS
#---------------------------------------------------------------
class EAS (object):
	def __init__ (self, handle=None, dll_path=None, log_file=None):
		if eas_dll is None:
			InitEASModule(dll_path)
		if log_file is not None:
			eas_logger.addHandler(log_file)
		eas_logger.debug('EAS.__init__')
		self.Init(handle)

	def __del__ (self):
		eas_logger.debug('EAS.__del__')
		self.Shutdown()

	def Init (self, handle=None):
		"""Initializes the EAS Library."""
		eas_logger.debug('EAS.Init')

		# if we are already initialized, shutdown first
		if hasattr(self, 'handle'):
			eas_logger.debug('EAS.Init called with library already initalized')
			self.ShutDown()

		# setup the logging function
		eas_dll.SetLogCallback(LogCallback)

		# create some members
		self.handle = c_void_p(0)
		self.audio_streams = []
		self.output_streams = []
		self.aux_mixer = None
		
		# create a sync lock
		self.lock = threading.RLock()
		with self.lock:
			# set log callback
		
			# get library configuration
			self.Config()

			# initialize library
			if handle is None:
				self.do_shutdown = True
				eas_logger.debug('Call EAS_Init')
				result = eas_dll.EAS_Init(byref(self.handle))
				if result:
					raise EAS_Exception(result, 'EAS_Init error %d' % result, 'EAS_Init')
			else:
				self.do_shutdown = False
				self.handle = handle

			# allocate audio buffer for rendering
			AudioBufferType = c_ubyte * (2 * self.config.mixBufferSize * self.config.numChannels)
			self.audio_buffer = AudioBufferType()
			self.buf_size = self.config.mixBufferSize

	def Config (self):
		"""Retrieves the EAS library configuration"""
		if not hasattr(self, 'config'):
			eas_logger.debug('Call EAS_Config')
			eas_dll.EAS_Config.restype = POINTER(EAS_Config)
			self.config = eas_dll.EAS_Config()[0]
		eas_logger.debug("libVersion=%08x, maxVoices=%d, numChannels=%d, sampleRate = %d, mixBufferSize=%d" %
			(self.config.libVersion, self.config.maxVoices, self.config.numChannels, self.config.sampleRate, self.config.mixBufferSize))

	def Shutdown (self):
		"""Shuts down the EAS library"""
		eas_logger.debug('EAS.Shutdown')
		if hasattr(self, 'handle'):
			with self.lock:
				# close audio streams
				audio_streams = self.audio_streams
				for f in audio_streams:
					eas_logger.warning('Stream was not closed before EAS_Shutdown')
					f.Close()

				# close output streams
				output_streams = self.output_streams
				for s in output_streams:
					s.close()

				# shutdown library
				if self.do_shutdown:
					eas_logger.debug('Call EAS_Shutdown')
					result = eas_dll.EAS_Shutdown(self.handle)
					if result:
						raise EAS_Exception(result, 'EAS_Shutdown error %d' % result, 'EAS_Shutdown')
				del self.handle

	def OpenFile (self, path):
		"""Opens an audio file to be played by the EAS library and
		returns an EAS_File object

		Arguments:
			path - path to audio file

		Returns:
			EAS_File
			
		"""
		with self.lock:
			eas_logger.debug('Call EAS_OpenFile for file: %s' % path)
			stream_handle = c_void_p(0)
			result = eas_dll.EAS_OpenFile(self.handle, path, byref(stream_handle))
			if result:
				raise EAS_Exception(result, 'EAS_OpenFile error %d on file %s' % (result, path), 'EAS_OpenFile')

			# create file object and save in list
			stream = EAS_File(path, stream_handle, self)
			self.audio_streams.append(stream)
			return stream

	def OpenMIDIStream (self, stream=None):
		"""Opens a MIDI stream.

		Arguments:
			stream - open stream object. If None, a new synth
			is created.

		Returns:
			EAS_MIDIStream
			
		"""
		with self.lock:
			eas_logger.debug('Call EAS_OpenMIDIStream')
			stream_handle = c_void_p(0)
			if stream.handle is not None:
				result = eas_dll.EAS_OpenMIDIStream(self.handle, byref(stream_handle), stream.handle)
			else:
				result = eas_dll.EAS_OpenMIDIStream(self.handle, byref(stream_handle), 0)
			if result:
				raise EAS_Exception(result, 'EAS_OpenMIDIStream error %d' % result, 'EAS_OpenMIDIStream')

			# create stream object and save in list
			stream = EAS_MIDIStream(stream_handle, self)
			self.audio_streams.append(stream)
			return stream

	def OpenToneControlStream (self, path):
		"""Opens an MMAPI tone control file to be played by the EAS
		library and returns an EAS_File object

		Arguments:
			path - path to audio file

		Returns:
			EAS_File
			
		"""
		with self.lock:
			eas_logger.debug('Call EAS_MMAPIToneControl for file: %s' % path)
			stream_handle = c_void_p(0)
			result = eas_dll.EAS_MMAPIToneControl(self.handle, path, byref(stream_handle))
			if result:
				raise EAS_Exception(result, 'EAS_MMAPIToneControl error %d on file %s' % (result, path), 'EAS_OpenToneControlStream')

			# create file object and save in list
			stream = EAS_File(path, stream_handle, self)
			self.audio_streams.append(stream)
			return stream

	def Attach (self, stream):
		"""Attach a file or output device to the EAS output.

		The stream object must support the following methods as
		defined in the Python wave module:
			close()
			setparams()
			writeframesraw()

		Arguments:
			stream - open wave object

		"""
		self.output_streams.append(stream)
		stream.setparams((self.config.numChannels, 2, self.config.sampleRate, 0, 'NONE', None))

	def Detach (self, stream):
		"""Detach a file or output device from the EAS output. See
		EAS.Attach for more details. It is the responsibility of
		the caller to close the wave file or stream.

		Arguments:
			stream - open and attached wave object
		"""
		self.output_streams.remove(stream)

	def StartWave (self, dev_num=0, sampleRate=None, maxBufSize=None):
		"""Route the audio output to the indicated wave device. Note
		that this can cause EASDLL.EAS_RenderWaveOut to return an
		error code if all the output buffers are full. In this case,
		the render thread should sleep a bit and try again.
		Unfortunately, due to the nature of the MMSYSTEM interface,
		there is no simple way to suspend the render thread.

		"""
		if sampleRate == None:
			sampleRate = self.config.sampleRate
		if maxBufSize == None:
			maxBufSize = self.config.mixBufferSize
		with self.lock:
			result = eas_dll.OpenWaveOutDevice(dev_num, sampleRate, maxBufSize)
			if result:
				raise EAS_Exception(result, 'OpenWaveOutDevice error %d' % result, 'OpenWaveOutDevice')

	def StopWave (self):
		"""Stop routing audio output to the audio device."""
		with self.lock:
			result = eas_dll.CloseWaveOutDevice()
			if result:
				raise EAS_Exception(result, 'CloseWaveOutDevice error %d' % result, 'CloseWaveOutDevice')

	def Render (self, count=None, secs=None):
		"""Calls EAS_Render to render audio.

		Arguments
			count - number of buffers to render
			secs - number of seconds to render

		If both count and secs are None, render a single buffer. 

		"""

		# determine number of buffers to render
		if count is None:
			if secs is not None:
				count = int(secs * float(self.config.sampleRate) / float(self.buf_size) + 0.5)
			else:
				count = 1

		# render buffers
		eas_logger.debug('rendering %d buffers' % count)
		samplesRendered = c_long(0)
		with self.lock:
			for c in range(count):
				# render a buffer of audio
				eas_logger.debug('rendering buffer')
				while 1:
					if self.aux_mixer is None:
						result = eas_dll.EAS_RenderWaveOut(self.handle, byref(self.audio_buffer), self.buf_size, byref(samplesRendered))
					else:
						result = eas_dll.EAS_RenderAuxMixer(self.handle, byref(self.audio_buffer), byref(samplesRendered))
					
					if result == 0:
						break;
					if result == EAS_BUFFER_FULL:
						time.sleep(0.01)
					else:
						raise EAS_Exception(result, 'EAS_Render error %d' % result, 'EAS_Render')

				# output to attached streams
				for s in self.output_streams:
					s.writeframesraw(self.audio_buffer)
				
	def GetRenderTime (self):
		"""Get the render time in seconds."""
		eas_logger.debug('Call EAS_GetRenderTime')
		msecs = c_int(0)
		with self.lock:
			result = eas_dll.EAS_GetRenderTime(self.handle, byref(msecs))
			if result:
				raise EAS_Exception(result, 'EAS_GetRenderTime error %d' % result, 'EAS_GetRenderTime')
		msecs = float(msecs.value) / 1000
		eas_logger.debug('EAS_GetRenderTime: time=%.3f' % msecs)
		return msecs

	def SetVolume (self, volume):
		"""Set the master volume"""
		eas_logger.debug('Call EAS_SetVolume: volume=%d' % volume)
		with self.lock:
			result = eas_dll.EAS_SetVolume(self.handle, 0, volume)
			if result:
					raise EAS_Exception(result, 'EAS_SetVolume error %d' % result, 'EAS_SetVolume')

	def GetVolume (self):
		"""Get the stream volume."""
		eas_logger.debug('Call EAS_GetVolume')
		volume = c_int(0)
		with self.lock:
			result = eas_dll.EAS_GetVolume(self.handle, 0, byref(volume))
			if result:
				raise EAS_Exception(result, 'EAS_GetVolume error %d' % result, 'EAS_GetVolume')
		eas_logger.debug('EAS_GetVolume: volume=%d' % volume.value)
		return volume.value

	def SetPolyphony (self, polyphony, synth_num=0):
		"""Set the polyphony of a synth."""
		eas_logger.debug('Call EAS_SetSynthPolyphony: synth_num=%d, polyphony=%d' % (synth_num, polyphony))
		with self.lock:
			result = eas_dll.EAS_SetSynthPolyphony(self.handle, synth_num, polyphony)
			if result:
				raise EAS_Exception(result, 'EAS_SetSynthPolyphony error %d on synth %d' % (result, synth_num), 'EAS_SetPolyphony')

	def GetPolyphony (self, synth_num=0):
		"""Get the polyphony of a synth."""
		eas_logger.debug('Call EAS_GetSynthPolyphony: synth_num=%d' % synth_num)
		polyphony = c_int(0)
		with self.lock:
			result = eas_dll.EAS_GetSynthPolyphony(self.handle, synth_num, byref(polyphony))
			if result:
				raise EAS_Exception(result, 'EAS_GetSynthPolyphony error %d on synth %d' % (result, synth_num), 'EAS_GetPolyphony')
		eas_logger.debug('Call EAS_GetSynthPolyphony: synth_num=%d, polyphony=%d' % (synth_num, polyphony.value))
		return polyphony.value

	def SetMaxLoad (self, max_load):
		"""Set the maximum parser load."""
		eas_logger.debug('Call EAS_SetMaxLoad: max_load=%d' % max_load)
		with self.lock:
			result = eas_dll.EAS_SetMaxLoad(self.handle, max_load)
			if result:
				raise EAS_Exception(result, 'EAS_SetMaxLoad error %d' % result, 'EAS_SetMaxLoad')

	def SetParameter (self, module, param, value):
		"""Set a module parameter."""
		eas_logger.debug('Call EAS_SetParameter: module=%d, param=%d, value=%d' % (module, param, value))
		with self.lock:
			result = eas_dll.EAS_SetParameter(self.handle, module, param, value)
			if result:
				raise EAS_Exception(result, 'EAS_SetParameter error %d (param=%d, value=%d)' % (result, param, value), 'EAS_SetParameter')
				
	def GetParameter (self, module, param):
		"""Get the polyphony of a synth."""
		eas_logger.debug('Call EAS_GetParameter: module=%d, param=%d' % (module, param))
		value = c_int(0)
		with self.lock:
			result = eas_dll.EAS_GetParameter(self.handle, module, param, byref(value))
			if result:
				raise EAS_Exception(result, 'EAS_SetParameter error %d (param=%d)' % (result, param), 'EAS_GetParameter')
		eas_logger.debug('Call EAS_SetParameter: module=%d, param=%d, value=%d' % (module, param, value.value))
		return value.value

	def SelectLib (self, test_lib=False):
		eas_logger.debug('Call EAS_SelectLib: test_lib=%s' % test_lib)
		easdll = cdll.LoadLibrary('EASDLL')
		with self.lock:
			result = eas_dll.EAS_SelectLib(self.handle, 0, test_lib)
			if result:
				raise EAS_Exception(result, 'EAS_SelectLib error %d' % result, 'EAS_SelectLib')

	def LoadDLSCollection (self, path):
		eas_logger.debug('Call EAS_LoadDLSCollection: lib_path=%s' % path)
		with self.lock:
			result = eas_dll.EAS_LoadDLSCollection(self.handle, 0, path)
			if result:
				raise EAS_Exception(result, 'EAS_LoadDLSCollection error %d lib %s' % (result, path), 'EAS_LoadDLSCollection')

	def SetAuxMixerHook (self, aux_mixer):

		# if aux mixer has bigger buffer, re-allocate buffer
		if (aux_mixer is not None) and (aux_mixer.buf_size > self.config.mixBufferSize):
			buf_size = aux_mixer.buf_size
		else:
			buf_size = self.config.mixBufferSize

		# allocate audio buffer for rendering
		AudioBufferType = c_ubyte * (2 * buf_size * self.config.numChannels)
		self.audio_buffer = AudioBufferType()
		self.buf_size = buf_size
		self.aux_mixer = aux_mixer

	def SetDebugLevel (self, level=3):
		"""Sets the EAS debug level."""
		with self.lock:
			eas_logger.debug('Call EAS_SetDebugLevel')
			eas_dll.EAS_DLLSetDebugLevel(self.handle, level)

#---------------------------------------------------------------
# EASAuxMixer
#---------------------------------------------------------------
class EASAuxMixer (object):
	def __init__ (self, eas=None, num_streams=3, sample_rate=44100, max_sample_rate=44100):
		eas_logger.debug('EASAuxMixer.__init__')
		self.Init(eas, num_streams, sample_rate, max_sample_rate)

	def __del__ (self):
		eas_logger.debug('EASAuxMixer.__del__')
		self.Shutdown()

	def Init (self, eas=None, num_streams=3, sample_rate=44100, max_sample_rate=44100):
		"""Initializes the EAS Auxilliary Mixer."""
		eas_logger.debug('EASAuxMixer.Init')

		if hasattr(self, 'eas'):
			raise EAS_Exception(-1, 'EASAuxMixer already initialized', 'EASAuxMixer.Init')

		# initialize EAS, if necessary
		if eas is None:
			eas_logger.debug('No EAS handle --- initializing EAS')
			eas = EAS()
			self.alloc_eas = True
		else:
			self.alloc_eas = False
		self.eas = eas

		# initialize library
		eas_logger.debug('Call EAS_InitAuxMixer')
		buf_size = c_int(0)
		result = eas_dll.EAS_InitAuxMixer(eas.handle, num_streams, sample_rate, max_sample_rate, byref(buf_size))
		if result:
			raise EAS_Exception(result, 'EAS_InitAuxMixer error %d' % result, 'EAS_InitAuxMixer')
		self.buf_size = buf_size.value
		self.streams = []
		eas.SetAuxMixerHook(self)

	def Shutdown (self):
		"""Shuts down the EAS Auxilliary Mixer"""
		eas_logger.debug('EASAuxMixer.Shutdown')
		if not hasattr(self, 'eas'):
			return
			
		with self.eas.lock:
			if len(self.streams):
				eas_logger.warning('Stream was not closed before EAS_ShutdownAuxMixer')
				for stream in self.streams:
					self.CloseStream(stream)

			self.eas.SetAuxMixerHook(None)

			# shutdown library
			eas_logger.debug('Call EAS_ShutdownAuxMixer')
			result = eas_dll.EAS_ShutdownAuxMixer(self.eas.handle)
			if result:
				raise EAS_Exception(result, 'EAS_ShutdownAuxMixer error %d' % result, 'EAS_ShutdownAuxMixer')

			# if we created the EAS reference here, shut it down
			if self.alloc_eas:
				self.eas.Shutdown()
				self.alloc_eas = False
			del self.eas

	def OpenStream (self, decoder_func, inst_data, sample_rate, num_channels):
		"""Opens an audio file to be played by the JET library and
		returns a JET_File object

		Arguments:
			callback - callback function to decode more audio

		"""
		with self.eas.lock:
			eas_logger.debug('Call EAS_OpenAudioStream')
			decoder_func = EAS_DECODER_FUNC(decoder_func)
			stream_handle = c_void_p(0)
			result = eas_dll.EAS_OpenAudioStream(self.eas.handle, decoder_func, inst_data, sample_rate, num_channels, stream_handle)
			if result:
				raise EAS_Exception(result, 'EAS_OpenAudioStream error %d on file %s' % (result, path), 'EAS_OpenAudioStream')
			self.streams.add(stream_handle)
			return stream_handle

	def CloseStream (self, stream_handle):
		"""Closes an open audio stream."""
		with self.eas.lock:
			eas_logger.debug('Call EAS_CloseAudioStream')
			result = eas_dll.JET_CloseFile(self.eas.handle, stream_handle)
			if result:
				raise EAS_Exception(result, 'EAS_CloseAudioStream error %d' % result, 'EAS_CloseAudioStream')

#---------------------------------------------------------------
# JET_Status 
#---------------------------------------------------------------
class JET_Status (Structure):
	_fields_ = [('currentUserID', c_int),
				('segmentRepeatCount', c_int),
				('numQueuedSegments', c_int),
				('paused', c_int),
				('location', c_long),
				('currentPlayingSegment', c_int),
				('currentQueuedSegment', c_int),
				]

#---------------------------------------------------------------
# JET_File
#---------------------------------------------------------------
class JET_File (object):
	def __init__ (self, handle, jet):
		eas_logger.debug('JET_File.__init__')
		self.handle = handle
		self.jet = jet

#---------------------------------------------------------------
# JET
#---------------------------------------------------------------
class JET (object):
	def __init__ (self, eas=None):
		# eas_logger.debug('JET.__init__')
		self.Init(eas)

	def __del__ (self):
		eas_logger.debug('JET.__del__')
		self.Shutdown()

	def Init (self, eas=None, config=None):
		"""Initializes the JET Library."""
		# eas_logger.debug('JET.Init')

		if hasattr(self, 'eas'):
			raise EAS_Exception(-1, 'JET library already initialized', 'Jet.Init')

		# create some members
		if eas is None:
			# eas_logger.debug('No EAS handle --- initializing EAS')
			eas = EAS()
			self.alloc_eas = True
		else:
			self.alloc_eas = False
		self.eas = eas
		self.fileOpen = False

		# handle configuration
		if config is None:
			config_handle = c_void_p(0)
			config_size = 0
		else:
			jet_config = S_JET_CONFIG()
			jet_config.appLowNote = config.appLowNote
			config_handle = c_void_p(jet_config)
			config_size = jet_config.sizeof()

		# initialize library
		# eas_logger.debug('Call JET_Init')
		result = eas_dll.JET_Init(eas.handle, config_handle, config_size)
		if result:
			raise EAS_Exception(result, 'JET_Init error %d' % result, 'JET_Init')

	def Shutdown (self):
		"""Shuts down the JET library"""
		eas_logger.debug('JET.Shutdown')
		if not hasattr(self, 'eas'):
			return
			
		with self.eas.lock:
			if self.fileOpen:
				eas_logger.warning('Stream was not closed before JET_Shutdown')
				self.CloseFile()

			# shutdown library
			eas_logger.debug('Call JET_Shutdown')
			result = eas_dll.JET_Shutdown(self.eas.handle)
			if result:
				raise EAS_Exception(result, 'JET_Shutdown error %d' % result, 'JET_Shutdown')

			# if we created the EAS reference here, shut it down
			if self.alloc_eas:
				self.eas.Shutdown()
				self.alloc_eas = False
			del self.eas

	def OpenFile (self, path):
		"""Opens an audio file to be played by the JET library and
		returns a JET_File object

		Arguments:
			path - path to audio file

		"""
		with self.eas.lock:
			eas_logger.debug('Call JET_OpenFile for file: %s' % path)
			result = eas_dll.JET_OpenFile(self.eas.handle, path)
			if result:
				raise EAS_Exception(result, 'JET_OpenFile error %d on file %s' % (result, path), 'JET_OpenFile')

	def CloseFile (self):
		"""Closes an open audio file."""
		with self.eas.lock:
			eas_logger.debug('Call JET_CloseFile')
			result = eas_dll.JET_CloseFile(self.eas.handle)
			if result:
				raise EAS_Exception(result, 'JET_CloseFile error %d' % result, 'JET_CloseFile')

	def QueueSegment (self, userID, seg_num, dls_num=-1, repeat=0, tranpose=0, mute_flags=0):
		"""Queue a segment for playback.

		Arguments:
			seg_num - segment number to queue
			repeat - repeat count (-1=repeat forever, 0=no repeat, 1+ = play n+1 times)
			tranpose - transpose amount (+/-12)
			
			"""
		with self.eas.lock:
			eas_logger.debug('Call JET_QueueSegment')
			result = eas_dll.JET_QueueSegment(self.eas.handle, seg_num, dls_num, repeat, tranpose, mute_flags, userID)
			if result:
				raise EAS_Exception(result, 'JET_QueueSegment error %d' % result, 'JET_QueueSegment')

	def Clear_Queue(self):
		"""Kills the queue."""
		with self.eas.lock:
			eas_logger.debug('Call JET_Clear_Queue')
			result = eas_dll.JET_Clear_Queue(self.eas.handle)
			if result:
				raise EAS_Exception(result, 'JET_Clear_Queue error %d' % result, 'JET_Clear_Queue')

	def GetAppEvent(self):
		"""Gets an App event."""
		with self.eas.lock:
			eas_logger.debug('Call JET_GetEvent')
			result = eas_dll.JET_GetEvent(self.eas.handle, 0, 0)
			return result    

	def Play(self):
		"""Starts JET playback."""
		with self.eas.lock:
			eas_logger.debug('Call JET_Play')
			result = eas_dll.JET_Play(self.eas.handle)
			if result:
				raise EAS_Exception(result, 'JET_Play error %d' % result, 'JET_Play')

	def Pause(self):
		"""Pauses JET playback."""
		with self.eas.lock:
			eas_logger.debug('Call JET_Pause')
			result = eas_dll.JET_Pause(self.eas.handle)
			if result:
				raise EAS_Exception(result, 'JET_Pause error %d' % result, 'JET_Pause')

	def Render (self, count=None, secs=None):
		"""Calls EAS_Render to render audio.

		Arguments
			count - number of buffers to render
			secs - number of seconds to render

		If both count and secs are None, render a single buffer. 

		"""
		# calls JET.Render
		with self.eas.lock:
			self.eas.Render(count, secs)

	def Status (self):
		"""Get JET status."""
		with self.eas.lock:
			eas_logger.debug('Call JET_Status')
			status = JET_Status()
			result = eas_dll.JET_Status(self.eas.handle, byref(status))
			if result:
				raise EAS_Exception(result, 'JET_Status error %d' % result, 'JET_Status')
			eas_logger.debug("currentUserID=%d, repeatCount=%d, numQueuedSegments=%d, paused=%d" %
				(status.currentUserID, status.segmentRepeatCount, status.numQueuedSegments, status.paused))
			return status
				
	def SetVolume (self, volume):
		"""Set the JET volume"""
		eas_logger.debug('Call JET_SetVolume')
		with self.eas.lock:
			result = eas_dll.JET_SetVolume(self.eas.handle, volume)
			if result:
					raise EAS_Exception(result, 'JET_SetVolume error %d' % result, 'JET_SetVolume')

	def SetTransposition (self, transposition):
		"""Set the transposition of a stream."""
		eas_logger.debug('Call JET_SetTransposition')
		with self.eas.lock:
			result = eas_dll.JET_SetTransposition(self.eas.handle, transposition)
			if result:
				raise EAS_Exception(result, 'JET_SetTransposition error %d' % result, 'JET_SetTransposition')

	def TriggerClip (self, clipID):
		"""Trigger a clip in the current segment."""
		eas_logger.debug('Call JET_TriggerClip')
		with self.eas.lock:
			result = eas_dll.JET_TriggerClip(self.eas.handle, clipID)
			if result:
				raise EAS_Exception(result, 'JET_SetTransposition error %d' % result, 'JET_TriggerClip')

	def SetMuteFlag (self, track_num, mute, sync=True):
		"""Trigger a clip in the current segment."""
		eas_logger.debug('Call JET_SetMuteFlag')
		with self.eas.lock:
			result = eas_dll.JET_SetMuteFlag(self.eas.handle, track_num, mute, sync)
			if result:
				raise EAS_Exception(result, 'JET_SetMuteFlag error %d' % result, 'JET_SetMuteFlag')

	def SetMuteFlags (self, mute_flags, sync=True):
		"""Trigger a clip in the current segment."""
		eas_logger.debug('Call JET_SetMuteFlags')
		with self.eas.lock:
			result = eas_dll.JET_SetMuteFlags(self.eas.handle, mute_flags, sync)
			if result:
				raise EAS_Exception(result, 'JET_SetMuteFlag error %d' % result, 'JET_SetMuteFlags')


