#
#	THIS FILE IS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL. SEE
#	THE 'COPYING' FILE FOR DETAILS
#
#	Instrument.py
#	
#	This module is the non-gui class the represents instruments. instruments
#	represent a track of audio that can contain many different sources in sequence.
#	This module handles loading and saving instruments from xml, the gstreamer
#	bits for playing and recording events, audio effects plugins, as well as any 
#	instrument specific functionality like; solo, mute, volume, etc.
#
#-------------------------------------------------------------------------------

import pygst
pygst.require("0.10")
import gst
import PlatformUtils
import os, time, shutil
import urlparse # To split up URI's
import gobject
import Event
import UndoSystem, IncrementalSave
import Utils

import Globals
import gettext
_ = gettext.gettext

#=========================================================================	

class Instrument(gobject.GObject):
	"""
	This module is the non-gui class the represents Instruments. Instruments
	represent a track of audio that can contain many different sources in sequence.
	It also handles loading and saving Instruments from xml, the gstreamer
	bits for playing and recording events, audio effects plugins, as well as any 
	Instrument specific functionality like; solo, mute, volume, etc.
	"""
	
	#_____________________________________________________________________
	
	"""
	Only elements that accept these caps will be work if added to the effects bin.
	This current must be audio/x-raw-float to prevent static from occuring
	when adding LADSPA effects while playing.
	"""
	LADSPA_ELEMENT_CAPS = "audio/x-raw-float, width=(int)32, rate=(int)[ 1, 2147483647 ], channels=(int)1, endianness=(int)1234"
	
	"""
	Signals:
		"arm" -- This instrument has been armed or dis-armed for recording.
		"effect" -- The effects for this instrument have changed. See below:
			"effect::added" -- An effect was added to this instrument.
			"effect::removed" -- An effect was removed from this instrument.
			"effect::reordered" -- An effect on this instrument has changed its position.
		"event" -- The events for this instrument have changed. The event ID will be passed as a parameter. See below:
			"event::added" -- An event was added to this instrument.
			"event::removed" -- An event was removed from this instrument.
		"image" -- The image for this instrument has changed.
		"mute" -- This instrument has been muted or unmuted.
		"name" -- The name of this instrument has changed.
		"recording-done" -- The event recording on this instrument has finished recording.
		"selected" -- This instrument has been selected or deselected.
		"visible" -- This instrument has been maximised or minimized in the mix view.
		"volume" -- The volume value for this instrument has changed.
	"""
	
	__gsignals__ = {
		"arm"		: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"effect"		: ( gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_DETAILED, gobject.TYPE_NONE, () ),
		"event"		: ( gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_DETAILED, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
		"image"		: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"mute"		: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"name"		: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"recording-done"	: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"selected"	: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"solo"		: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"visible"	: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
		"volume"		: ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () )
	}
	
	def __init__(self, project, name, type, pixbuf, id=None):
		"""
		Creates a new instance of Instrument.
		
		Parameters:
			project -- the currently active Project.
			name -- name of the Instrument.
			type -- type of the Instrument.
			pixbuf -- image of the Instrument resource object.
			id -- unique ID value for the Instrument.
		"""
		gobject.GObject.__init__(self)
		
		self.project = project
		
		self.events = []				# List of events attached to this instrument
		self.graveyard = []			# List of events that have been deleted (kept for undo)
		self.effects = []				# List of GStreamer effect elements
		
		self.name = name			# Name of this instrument
		self.pixbuf = pixbuf			# The icon pixbuf resource
		self.instrType = type		# The type of instrument
		
		self.isArmed = False			# True if the instrument is armed for recording
		self.isMuted = False			# True if the "mute" button is toggled on
		self.actuallyIsMuted = False	# True if the instrument is muted (silent)
		self.isSolo = False			# True if the instrument is solo'd (only instrument active)
		self.isVisible = True			# True if the instrument should be displayed in the mixer views
		self.isSelected = False		# True if the instrument is currently selected
		
		self.level = 0.0				# Current audio level in range 0..1
		self.volume = 1.0			# Gain of the current instrument in range 0..1
		self.pan = 0.0				# pan number (between -100 and 100)
		self.currentchainpreset = None	# current instrument wide chain preset
		self.output = ""
		self.recordingbin = None
		self.id = project.GenerateUniqueID(id)	#check is id is already being used before setting
		
		self.input = None	# the device to use for recording on this instrument.
		self.inTrack = 0	# Input track to record from if device is multichannel.
	
		# CREATE GSTREAMER ELEMENTS #
		self.playbackbin = gst.element_factory_make("bin", "Instrument_%d"%self.id)
		self.volumeElement = gst.element_factory_make("volume", "Instrument_Volume_%d"%self.id)
		self.levelElement = gst.element_factory_make("level", "Instrument_Level_%d"%self.id)
		self.panElement = gst.element_factory_make("audiopanorama", "Instrument_Pan_%d"%self.id)
		self.resample = gst.element_factory_make("audioresample")
		
		self.composition = gst.element_factory_make("gnlcomposition")
		self.silentGnlSource = gst.element_factory_make("gnlsource")		# the default source that makes the silence between the tracks
		self.silenceAudioSource = gst.element_factory_make("audiotestsrc")
		
		self.effectsBin = gst.element_factory_make("bin", "InstrumentEffects_%d"%self.id)
		self.effectsBinConvert = gst.element_factory_make("audioconvert", "Start_Effects_Converter_%d"%self.id)
		self.effectsBinCaps = gst.element_factory_make("capsfilter", "Effects_float_caps_%d"%self.id)
		self.effectsBinCaps.set_property("caps", gst.Caps(self.LADSPA_ELEMENT_CAPS))
		self.effectsBinEndConvert = gst.element_factory_make("audioconvert", "End_Effects_Converter_%d"%self.id)
		
		self.volumeFadeBin = gst.element_factory_make("bin", "Volume_fades_bin")
		self.volumeFadeElement = gst.element_factory_make("volume", "Volume_Fade_Element")
		self.volumeFadeStartConvert = gst.element_factory_make("audioconvert", "Start_fadebin_converter")	
		self.volumeFadeEndConvert = gst.element_factory_make("audioconvert", "End_fadebin_converter")
		self.volumeFadeOperation = gst.element_factory_make("gnloperation", "gnloperation")
		self.volumeFadeController = gst.Controller(self.volumeFadeElement, "volume")
		
		# CREATE GHOSTPADS FOR BINS #
		self.effectsBin.add(self.effectsBinConvert, self.effectsBinCaps, self.effectsBinEndConvert)
		self.effectsBinSink = gst.GhostPad("sink", self.effectsBinConvert.get_pad("sink"))
		self.effectsBin.add_pad(self.effectsBinSink)
		self.effectsBinSrc = gst.GhostPad("src", self.effectsBinEndConvert.get_pad("src"))
		self.effectsBin.add_pad(self.effectsBinSrc)
		
		self.volumeFadeBin.add(self.volumeFadeElement)
		self.volumeFadeBin.add(self.volumeFadeStartConvert)
		self.volumeFadeBin.add(self.volumeFadeEndConvert)
		volumeFadeBinSink = gst.GhostPad("sink", self.volumeFadeStartConvert.get_pad("sink"))
		self.volumeFadeBin.add_pad(volumeFadeBinSink)
		volumeFadeBinSrc = gst.GhostPad("src", self.volumeFadeEndConvert.get_pad("src"))
		self.volumeFadeBin.add_pad(volumeFadeBinSrc)
		
		# SET ELEMENT PROPERTIES #
		self.levelElement.set_property("interval", gst.SECOND / 50)
		self.levelElement.set_property("message", True)
		self.levelElement.set_property("peak-ttl", 0)
		self.levelElement.set_property("peak-falloff", 20)
		
		self.panElement.set_property("panorama", 0)

		self.silenceAudioSource.set_property("wave", 4)	#4 is silence
		
		self.silentGnlSource.set_property("priority", 2 ** 32 - 1)
		self.silentGnlSource.set_property("start", 0)
		self.silentGnlSource.set_property("duration", 1000 * gst.SECOND)
		self.silentGnlSource.set_property("media-start", 0)
		self.silentGnlSource.set_property("media-duration", 1000 * gst.SECOND)
		
		self.volumeFadeOperation.set_property("start", long(0) * gst.SECOND)
		self.volumeFadeOperation.set_property("duration", long(20) * gst.SECOND)
		self.volumeFadeOperation.set_property("priority", 1)
		
		self.volumeFadeController.set_interpolation_mode("volume", gst.INTERPOLATE_LINEAR)
		
		# ADD ELEMENTS TO THE PIPELINE AND/OR THEIR BINS #
		self.playbackbin.add(self.volumeElement, self.levelElement, self.panElement, self.resample)
		self.playbackbin.add(self.composition)
		self.playbackbin.add(self.effectsBin)
		
		self.volumeFadeOperation.add(self.volumeFadeBin)
		self.silentGnlSource.add(self.silenceAudioSource)
		self.composition.add(self.silentGnlSource)
		self.composition.add(self.volumeFadeOperation)
		
		# LINK GSTREAMER ELEMENTS #
		self.effectsBinConvert.link(self.effectsBinCaps)
		self.effectsBinCaps.link(self.effectsBinEndConvert)
		
		self.effectsBin.link(self.volumeElement)
		self.volumeElement.link(self.levelElement)
		self.levelElement.link(self.panElement)	
		self.panElement.link(self.resample)

		self.playghostpad = gst.GhostPad("src", self.resample.get_pad("src"))
		self.playbackbin.add_pad(self.playghostpad)
		
		self.volumeFadeStartConvert.link(self.volumeFadeElement)
		self.volumeFadeElement.link(self.volumeFadeEndConvert)
		
		self.AddAndLinkPlaybackbin()

		self.composition.connect("pad-added", self.__PadAddedCb)
		self.composition.connect("pad-removed", self.__PadRemovedCb)
		
		#mute this instrument if another one is solo
		self.OnMute()
		#set the volume element since it depends on the project's volume as well
		self.UpdateVolume()
		
	#_____________________________________________________________________
	
	def __PadAddedCb(self, element, pad):
		"""
		Links a new pad to the rest of the playbackbin when one is created
		by the composition.
		
		Parameters:
			element -- GStreamer element calling this function.
			pad -- newly added pad object.
		"""
		Globals.debug("NEW PAD on instrument %s" % self.name)
		convpad = self.effectsBin.get_compatible_pad(pad, pad.get_caps())
		pad.link(convpad)

	#_____________________________________________________________________

	def __PadRemovedCb(self, element, pad):
		"""
		Removes a GStreamer pad from the specified instrument.
		
		Parameters:
			element -- GStreamer element calling this function.
			pad -- pad to be removed from the Instrument.
		"""
		Globals.debug("pad removed on instrument %s" % self.name)
		self.composition.set_state(gst.STATE_READY)

	#_____________________________________________________________________
	
	def __repr__(self):
		"""
		Creates a representation string of the Instrument.

		Returns:
			a string representing the id and name for this Instrument.
		"""
		return "Instrument [%d] %s"%(self.id, self.name)
		
	#_____________________________________________________________________
		
	def StoreToXML(self, doc, parent, graveyard=False):
		"""
		Converts this Instrument into an XML representation suitable for saving to a file.

		Parameters:
			doc -- the XML document object the Instrument will be saved to.
			parent -- the parent node that the serialized Instrument should
						be added to.
			graveyard -- True if this Instrument is on the graveyard stack,
						and should be serialized as a dead Instrument.
		"""
		if graveyard:
			ins = doc.createElement("DeadInstrument")
		else:
			ins = doc.createElement("Instrument")
		parent.appendChild(ins)
		ins.setAttribute("id", str(self.id))
		
		items = ["name", "isArmed", 
				"isMuted", "isSolo", "input", "output", "volume",
				"isSelected", "isVisible", "inTrack", "instrType", "pan"]
		
		params = doc.createElement("Parameters")
		ins.appendChild(params)
		
		Utils.StoreParametersToXML(self, doc, params, items)
		
		for effect in self.effects:
			globaleffect = doc.createElement("GlobalEffect")
			globaleffect.setAttribute("element", effect.get_factory().get_name())
			ins.appendChild(globaleffect)
		
			propsdict = {}
			for prop in gobject.list_properties(effect):
				if prop.flags & gobject.PARAM_WRITABLE:
					propsdict[prop.name] = effect.get_property(prop.name)
			
			Utils.StoreDictionaryToXML(doc, globaleffect, propsdict)
			
		for ev in self.events:
			ev.StoreToXML(doc, ins)
		for ev in self.graveyard:
			ev.StoreToXML(doc, ins, graveyard=True)

	#_____________________________________________________________________

	def AddEffect(self, effectName):
		"""
		Adds an effect to the pipeline for this Instrument.
		
		Considerations:
			The effect is always placed in the pipeline after any other
			effects that were previously added.
			
		Parameters:
			effectName -- GStreamer element name of the effect to add.
			
		Returns:
			the added effect element.
		"""
		#make the new effect
		effectElement = gst.element_factory_make(effectName)
		self.effects.append(effectElement)
		#add the element to effects bin
		self.effectsBin.add(effectElement)
		
		# The src pad on the first element (an audioconvert) in the bin
		startSrcPad = self.effectsBinConvert.get_pad("src")
		
		state = self.playbackbin.get_state(0)[1]
		if state == gst.STATE_PAUSED or state == gst.STATE_PLAYING:
			startSrcPad.set_blocked(True)
			
		lastEffectElement = self.effectsBinEndConvert.get_pad("sink").get_peer().get_parent()
		
		lastEffectElement.unlink(self.effectsBinEndConvert)
		lastEffectElement.link(effectElement)
		effectElement.link(self.effectsBinEndConvert)
		
		# make the element's state match the bin's state
		effectElement.set_state(state)
		#give it a lambda for a callback that does nothing, so we don't have to wait
		startSrcPad.set_blocked_async(False, lambda x,y: False)
		
		self.emit("effect::added")
		
		return effectElement
	
	#_____________________________________________________________________
	
	def RemoveEffect(self, effect):
		"""
		Remove the given GStreamer element from the effects bin.
		
		Parameters:
			effect -- GStreamer effect to be removed from this Instrument.
		"""
		if effect not in self.effects:
			Globals.debug("Error: trying to remove an element that is not in the list")
			return
		
		for pad in effect.sink_pads():
			if pad.is_linked():
				previousElement = pad.get_peer().get_parent()
				break
		
		for pad in effect.src_pads():
			if pad.is_linked():
				nextElement = pad.get_peer().get_parent()
				break
		
		# The src pad on the first element (an audioconvert) in the bin
		startSrcPad = self.effectsBinConvert.get_pad("src")
		
		state = self.playbackbin.get_state(0)[1]
		if state == gst.STATE_PAUSED or state == gst.STATE_PLAYING:
			startSrcPad.set_blocked(True)
		
		previousElement.unlink(effect)
		effect.unlink(nextElement)
		previousElement.link(nextElement)
		
		# Remove and dispose of the element
		self.effectsBin.remove(effect)
		effect.set_state(gst.STATE_NULL)
		#remove the effect from our own list
		self.effects.remove(effect)
		
		#give it a lambda for a callback that does nothing, so we don't have to wait
		startSrcPad.set_blocked_async(False, lambda x,y: False)
		
		self.emit("effect::removed")
	
	#_____________________________________________________________________
	
	def ChangeEffectOrder(self, effect, newPosition):
		"""
		Move a given GStreamer element inside the effects bin. This method
		does not swap the element into its new position, it simply shifts all the
		elements between the effect's current position and the new position
		down by one. Since a Gstreamer pipeline is essentially a linked list, this
		"shift" implementation is the fastest way to make one element move to
		a new position without changing the order of any of the others.
		For example if this instrument has five effects which are ordered
		A, B, C, D, E, and I call this method with effect D and a new position
		of 1 the new order will be:  A, D, B, C, E.
		
		Parameters:
			effect -- GStreamer effect to be moved.
			newPosition -- value of the new position inside the effects bin
					the effect will have (with 0 as the first position).
		"""
		if effect not in self.effects:
			Globals.debug("Error: trying to remove an element that is not in the list")
			return
		if newPosition >= len(self.effects):
			Globals.debug("Error: trying to move effect to position past the end of the list")
			return
			
		oldPosition = self.effects.index(effect)
		if oldPosition == newPosition:
			#the effect is already in the proper position
			return
		
		# The effect currently at the position we want to move the given effect to
		newPositionEffect = self.effects[newPosition]
		
		for pad in effect.sink_pads():
			if pad.is_linked():
				previousElement = pad.get_peer().get_parent()
				break
		
		for pad in effect.src_pads():
			if pad.is_linked():
				nextElement = pad.get_peer().get_parent()
				break
		
		# The src pad on the first element (an audioconvert) in the bin
		startSrcPad = self.effectsBinConvert.get_pad("src")
		# check the state and block if we have to
		state = self.playbackbin.get_state(0)[1]
		if state == gst.STATE_PAUSED or state == gst.STATE_PLAYING:
			startSrcPad.set_blocked(True)
			
		#here's where we start unlinking and relinking
		previousElement.unlink(effect)
		effect.unlink(nextElement)
		previousElement.link(nextElement)
		
		if oldPosition > newPosition:
			for pad in newPositionEffect.sink_pads():
				if pad.is_linked():
					newPositionPrevious = pad.get_peer().get_parent()
					break
			
			newPositionPrevious.unlink(newPositionEffect)
			newPositionPrevious.link(effect)
			effect.link(newPositionEffect)
		else:
			for pad in newPositionEffect.src_pads():
				if pad.is_linked():
					newPositionNext = pad.get_peer().get_parent()
					break
			
			newPositionEffect.unlink(newPositionNext)
			newPositionEffect.link(effect)
			effect.link(newPositionNext)
			
		# remove and insert to our own llst so it matches the changes just made
		del self.effects[oldPosition]
		self.effects.insert(newPosition, effect)
		
		#give it a lambda for a callback that does nothing, so we don't have to wait
		startSrcPad.set_blocked_async(False, lambda x,y: False)
		
		self.emit("effect:reordered")
	
	#_____________________________________________________________________
	
	def GetRecordingEvent(self):
		"""
		Obtain an Event suitable for recording.
		Returns:
			an Event suitable for recording.
		"""
		event = Event.Event(self)
		event.start = self.project.transport.GetPosition()
		event.isRecording = True
		event.name = _("Recorded audio")
		
		ext = Globals.settings.recording["file_extension"]
		filename = "%s_%d.%s" % (Globals.FAT32SafeFilename(self.name), event.id, ext)
		event.file = filename
		event.levels_file = filename + Event.Event.LEVELS_FILE_EXTENSION
		
		inc = IncrementalSave.NewEvent(self.id, filename, event.start, event.id, recording=True)
		self.project.SaveIncrementalAction(inc)
		
		#must add it to the instrument's list so that an update of the event lane will not remove the widget
		self.events.append(event)
		self.emit("event::added", event)
		return event

	#_____________________________________________________________________

	@UndoSystem.UndoCommand("DeleteEvent", "temp", incremental_save=False)
	def FinishRecordingEvent(self, event):
		"""
		Called to log the adding of this event on the undo stack
		and to properly load the file that has just been recorded.
		
		Parameters:
			event -- Event object that has finished being recorded.
		"""
		event.isRecording = False
		event.GenerateWaveform()
		self.temp = event.id
		self.emit("recording-done")
	
	#_____________________________________________________________________
	
	def FinalizeRecording(self, event):
		"""
		Called when the recording of an Event has finished.
		
		Parameters:
			event -- Event object that has finished being recorded.
		"""
		#create our undo action to make everything atomic
		undoAction = self.project.NewAtomicUndoAction()
		#make sure the event will act mormally (like a loaded file) now
		self.FinishRecordingEvent(event, _undoAction_=undoAction)
		# remove all the events behind the recorded event (because we can't have overlapping events.
		self.RemoveEventsUnderEvent(event, undoAction)
		
	#_____________________________________________________________________
	
	def AddEventsFromList(self, start, fileList, copyFile=False, undoAction=None):
		"""
		Adds multiple files to an instrument one after another starting
		at the given start position.
		
		Parameters:
			start -- the offset time in seconds for the first event.
			fileList -- URIs to the Event files.
			copyfile --	True = copy the files to Project's audio directory.
						False = don't copy the files to the Project's audio directory.
		"""
		if not fileList:
			return
			
		if not undoAction:
			undoAction = self.project.NewAtomicUndoAction()
			
		for uri in fileList:
			# Parse the uri, and continue only if it is pointing to a local file
			(scheme, domain, file, params, query, fragment) = urlparse.urlparse(uri, "file", False)
			if scheme == "file":
				file = PlatformUtils.url2pathname(file)
				event = self.addEventFromFile(start, file, copyFile, _undoAction_=undoAction)
			else:
				event = self.addEventFromURL(start, uri, _undoAction_=undoAction)
			
			if event:
				event.MoveButDoNotOverlap(event.start)
				event.SetProperties()
				start += event.duration
	
	#_____________________________________________________________________

	@UndoSystem.UndoCommand("DeleteEvent", "temp", incremental_save=False)
	def addEventFromFile(self, start, file, copyfile=False, name=None, duration=None, levels_file=None):
		"""
		Adds an Event from a file to this Instrument.
		
		Parameters:
			start -- the offset time in seconds for the Event.
			file -- path to the Event file.
			copyfile --	True = copy the file to Project's audio directory.
						False = don't copy the file to the Project's audio directory.
			name -- An optional user visible name. The filename will be used if None.
						
		Returns:
			the added Event.
		"""
		filelabel=file
		event_id = self.project.GenerateUniqueID(None,  reserve=False)
		if not name:
			name = os.path.basename(file)
		root,  extension = os.path.splitext(name.replace(" ", "_"))
		
		if extension:
			newfile = "%s_%d%s" % (root, event_id, extension)
		else:
			newfile = "%s_%d" % (root, event_id)

		if copyfile:
			audio_file = os.path.join(self.project.audio_path, newfile)
			
			try:
				shutil.copyfile(file, audio_file)
			except IOError:
				raise UndoSystem.CancelUndoCommand()
				
			self.project.deleteOnCloseAudioFiles.append(audio_file)
			inc = IncrementalSave.NewEvent(self.id, newfile, start, event_id)
			self.project.SaveIncrementalAction(inc)
			
			file = newfile
		else:
			inc = IncrementalSave.NewEvent(self.id, file, start, event_id)
			self.project.SaveIncrementalAction(inc)

		ev = Event.Event(self, file, event_id, filelabel)
		ev.start = start
		ev.name = name
		self.events.append(ev)
		
		if duration and levels_file:
			ev.duration = duration
			ev.levels_file = levels_file
			ev.levels_list.fromfile(ev.GetAbsLevelsFile())
			# update properties and position when duration changes.
			ev.MoveButDoNotOverlap(ev.start)
			ev.SetProperties()
		else:
			ev.GenerateWaveform()

		self.temp = ev.id
		
		self.emit("event::added", ev)
		
		return ev
		
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("DeleteEvent", "temp", incremental_save=False)
	def addEventFromURL(self, start, url):
		"""
		Adds an Event from a URL to this Instrument.
		
		Considerations:
			Unlike addEventFromFile, there is no copyfile option here,
			because it's mandatory.
		
		Parameters:
			start -- The offset time in seconds for the Event.
			url -- url of the Event to be added.
			
		Returns:
			the added Event.
		"""
		event_id = self.project.GenerateUniqueID(None,  reserve=False)
		# no way of knowing whether there's a filename, so make one up
		newfile = str(event_id)
		
		audio_file = os.path.join(self.project.audio_path, newfile)
		self.project.deleteOnCloseAudioFiles.append(audio_file)
		
		# Create the event now so we can return it, and fill in the file later
		ev = Event.Event(self, newfile, event_id, url)
		ev.start = start
		ev.name = os.path.split(audio_file)[1]
		ev.isDownloading = True
		self.events.append(ev)
		
		Globals.debug("Event data downloading...")
		result = ev.CopyAndGenerateWaveform(url)
		
		if not result:
			self.events.remove(ev)
			raise UndoSystem.CancelUndoCommand()
		
		inc = IncrementalSave.StartDownload(self.id, url, newfile, start, event_id)
		self.project.SaveIncrementalAction(inc)
		
		self.temp = ev.id
		self.emit("event::added", ev)
		
		return ev
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("DeleteEvent", "temp")
	def addEventFromEvent(self, start, event):
		"""
		Creates a new Event instance identical to the given Event object
		and adds it to this instrument (for paste functionality).
		
		Parameters:
			start - the offset time in seconds for the new Event.
			event - the Event to be cloned on this instrument.
		"""
		ev = Event.Event(self, event.file)
		ev.start = start
		for i in ["duration", "name", "offset"]:
			setattr(ev, i, getattr(event, i))
		ev.levels_list = event.levels_list.copy()
		ev._Event__fadePointsDict = event._Event__fadePointsDict.copy()
		ev._Event__UpdateAudioFadePoints()
		
		self.events.append(ev)
		ev.SetProperties()
		ev.MoveButDoNotOverlap(ev.start)
		
		self.temp = ev.id
		self.emit("event::added", ev)
		return ev
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("ResurrectEvent", "temp")
	def DeleteEvent(self, eventid):
		"""
		Removes an Event from this Instrument.
			
		Parameters:
			eventid -- ID of the Event to be removed.
		"""
		event = [x for x in self.events if x.id == eventid][0]
		
		self.graveyard.append(event)
		self.events.remove(event)
		event.DestroyFilesource()
		event.StopGenerateWaveform(False)
		
		self.temp = eventid
		self.emit("event::removed", event)
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("DeleteEvent", "temp")
	def ResurrectEvent(self, eventid):
		"""
		Brings an Event back from the graveyard.
		
		Parameters:
			eventid -- ID of the Event to be resurrected.
		"""
		event = [x for x in self.graveyard if x.id == eventid][0]
		
		self.events.append(event)
		self.graveyard.remove(event)
		event.CreateFilesource()
		if event.isLoading or not event.levels_list:
			event.GenerateWaveform()
		
		self.temp = eventid
		self.emit("event::added", event)
	
	#_____________________________________________________________________

	def MultipleEventsSelected(self):
		"""
		Confirms whether or not multiple events are selected.
		
		Returns:
			True = multiple Instruments have been selected.
			False = none or just one Instrument has been seleced.
		"""
		multiple = 0
		for ev in self.events:
			if (ev.isSelected):
				multiple += 1
		return (multiple > 1)

	#_____________________________________________________________________

	def SetLevel(self, level):
		"""
		Sets the level of this Instrument.
		
		Considerations:
			This sets the current REPORTED level, NOT THE VOLUME!
		
		Parameters:
			level -- new level value in a [0,1] range.
		"""
		self.level = level
	
	#_____________________________________________________________________

	def SetVolume(self, volume):
		"""
		Sets the volume of this Instrument.
		
		Parameters:
			volume -- new volume value in a [0,1] range.
		"""
		if self.volume != volume:
			self.volume = volume
			self.UpdateVolume()
			self.emit("volume")

	#_____________________________________________________________________
	
	def CommitVolume(self):
		"""
		Signal that the volume is no longer volatile. This means we can incrementally save,
		which we didn't do before because the volume was rapidly changing as the user
		moved the mouse.
		"""
		inc = IncrementalSave.InstrumentSetVolume(self.id, self.volume)
		self.project.SaveIncrementalAction(inc)
		
	#_____________________________________________________________________
	
	def UpdateVolume(self):
		"""
		Updates the volume property of the gstreamer volume element
		based on this instrument's volume and the project's master volume.
		"""
		volume = self.volume * self.project.volume
		self.volumeElement.set_property("volume", volume)
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("SetName", "temp")
	def SetName(self, name):
		"""
		Sets the Instrument's name so it can be registered in the undo stack.
		
		Parameters:
			name -- string with the name for the Instrument.
		"""
		if self.name != name:
			self.temp = self.name
			self.name = name
			self.emit("name")
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("ToggleArmed")
	def ToggleArmed(self):
		"""
		Arms/Disarms the Instrument for recording.
		"""
		self.isArmed = not self.isArmed
		self.emit("arm")
		
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("ToggleMuted", "temp")
	def ToggleMuted(self, wasSolo):
		"""
		Mutes/Unmutes the Instrument.
		
		Parameters:
			wasSolo --	True = the Instrument had solo mode enabled.
						False = the Instrument was not in solo mode.
						
		Considerations:
			The signal "mute" is not emitted here because it is emitted in
			the OnMute() function.
		"""
		self.temp = self.isSolo
		self.isMuted = not self.isMuted
		if self.isSolo and not wasSolo:
			self.isSolo = False
			self.project.soloInstrCount -= 1
			self.project.OnAllInstrumentsMute()
		elif not self.isSolo and wasSolo:
			self.isSolo = True
			self.project.soloInstrCount += 1
			self.project.OnAllInstrumentsMute()
		else:
			self.OnMute()
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("ToggleSolo", "temp")
	def ToggleSolo(self, wasMuted):
		"""
		Mutes/Unmutes the other Instruments in the Project.
		
		Parameters:
			wasMuted --	True = the Instrument was muted.
						False = the Instrument was not muted.
		"""
		self.temp = self.isMuted
		self.isMuted = wasMuted
		
		if self.isSolo:
			self.isSolo = False
			self.project.soloInstrCount -= 1
		else:
			self.isSolo = True
			self.project.soloInstrCount += 1
		
		self.project.OnAllInstrumentsMute()
		self.emit("solo")
	
	#_____________________________________________________________________
	
	@UndoSystem.UndoCommand("SetVisible", "temp")
	def SetVisible(self, visible):
		"""
		Sets whether the Instrument is minimized in the CompactMixView.
		
		Parameters:
			visible --	True = the Instrument should be hidden in the mixing view.
						False = the Instrument should be shown normally in 
								the mixing view.
		"""
		if self.isVisible != visible:
			self.temp = self.isVisible
			self.isVisible = visible
			self.emit("visible")
	
	#_____________________________________________________________________
	
	def SetSelected(self, sel):
		"""
		Sets the Instrument to be highlighted and receive keyboard actions.
		
		Parameters:
			sel -- 	True = the Instrument is currently selected.
					False = the Instrument is not currently selected.
		"""
		# No need to emit signal when there is no change in selection state
		if self.isSelected is not sel:
			self.isSelected = sel
			self.emit("selected")
	
	#_____________________________________________________________________
	
	def SetInput(self, device, inTrack):
		if device != self.input or inTrack != self.inTrack:
			self.input = device
			self.inTrack = inTrack
			
			inc = IncrementalSave.InstrumentSetInput(self.id, device, inTrack)
			self.project.SaveIncrementalAction(inc)
			
	#_____________________________________________________________________
	
	def OnMute(self):
		"""
		Updates the GStreamer volume element to reflect the mute status.
		"""
		self.checkActuallyIsMuted()
		if self.actuallyIsMuted:
			self.volumeElement.set_property("mute", True)
		else:
			self.volumeElement.set_property("mute", False)
		
		self.emit("mute")
	
	#_____________________________________________________________________
	
	def checkActuallyIsMuted(self):
		"""
		Determines if this Intrument should be muted, by taking into account
		if any other Intruments are muted.
		"""
		if self.isMuted:
			self.actuallyIsMuted = True
		elif self.isSolo:
			self.actuallyIsMuted = False
		elif self.project.soloInstrCount > 0:
			self.actuallyIsMuted = True
		else:
			self.actuallyIsMuted = False
	
	#_____________________________________________________________________
	
	def AddAndLinkPlaybackbin(self):
		"""
		Creates a playback bin for this Instrument and adds it to the main
		playback pipeline. *CHECK*
		"""
		#make sure our playbackbin is in the same state so the pipeline can continue what it was doing
		status, state, pending = self.project.playbackbin.get_state(0)
		if pending != gst.STATE_VOID_PENDING:
			self.playbackbin.set_state(pending)
		else:
			self.playbackbin.set_state(state)
			
		if not self.playbackbin in list(self.project.playbackbin.elements()):
			self.project.playbackbin.add(self.playbackbin)
			Globals.debug("added instrument playbackbin to adder playbackbin", self.id)
		if not self.playghostpad.get_peer():
			self.playbackbin.link(self.project.adder)
			#give it a lambda for a callback that does nothing, so we don't have to wait
			self.playghostpad.set_blocked_async(False, lambda x,y: False)
			Globals.debug("linked instrument playbackbin to adder (project)")

	#_____________________________________________________________________
	
	def RemoveAndUnlinkPlaybackbin(self):
		"""
		Removes this Instrumen's playback bin from the main playback pipeline. *CHECK*
		"""
		#get reference to pad before removing self.playbackbin from project.playbackbin!
		pad = self.playghostpad.get_peer()
		
		if pad:
			status, state, pending = self.playbackbin.get_state(0)
			if state == gst.STATE_PAUSED or state == gst.STATE_PLAYING or \
					pending == gst.STATE_PAUSED or pending == gst.STATE_PLAYING:
				self.playghostpad.set_blocked(True)
			self.playbackbin.unlink(self.project.adder)
			self.project.adder.release_request_pad(pad)
			Globals.debug("unlinked instrument playbackbin from adder")
		
		if self.playbackbin in list(self.project.playbackbin.elements()):
			self.project.playbackbin.remove(self.playbackbin)
			Globals.debug("removed instrument playbackbin from project playbackbin")
	
	#_____________________________________________________________________

	@UndoSystem.UndoCommand("ChangeType", "temp", "temp2")
	def ChangeType(self, type, name):
		"""
		Changes the Intrument's type and name.
		
		Considerations:
			The given type must be loaded in the Instrument list
			in Globals or the image will not be found.
			
		Parameters:
			type -- new type for this Instrument.
			name -- new name for this Instrument.
		"""
		
		self.temp = self.instrType
		self.temp2 = self.name
		
		pixbuf = Globals.getCachedInstrumentPixbuf(type)
		if type == self.instrType or not pixbuf:
			raise UndoSystem.CancelUndoCommand()

		for tuple_ in Globals.getCachedInstruments():
			if tuple_[1] == self.instrType:
				if tuple_[0] == self.name:
					#if the instrument name has not been modified by the user, we can replace it.
					self.name = name
					self.emit("name")
				break

		self.instrType = type
		self.pixbuf = pixbuf

		self.emit("image")

	#_____________________________________________________________________

	def PrepareController(self):
		"""
		Fills the gst.Controller for this Instrument with its list of fade times.
		"""
		
		Globals.debug("Preparing the controller")
		# set the length of the operation to be the full length of the project
		self.volumeFadeOperation.set_property("duration", self.project.GetProjectLength() * gst.SECOND)
		self.volumeFadeController.unset_all("volume")
		firstpoint = False
		for ev in self.events:
			if not ev.audioFadePoints:
				#there are no fade points, so just make it 100% all the way through
				for point, vol in ((ev.start, 0.99), (ev.start+ev.duration, 0.99)):
					Globals.debug("FADE POINT: time(%.2f) vol(%.2f)" % (point, vol))
					self.volumeFadeController.set("volume", (point) * gst.SECOND, vol)
				continue
			
			for point in ev.audioFadePoints:
				if ev.start + point[0] == 0.0:
					firstpoint = True
				#FIXME: remove vol=0.99 when gst.Controller is fixed to accept many consecutive 1.0 values.
				if point[1] == 1.0:
					vol = 0.99
				else:
					vol = point[1]
				Globals.debug("FADE POINT: time(%.2f) vol(%.2f)" % (ev.start + point[0], vol))
				self.volumeFadeController.set("volume", (ev.start + point[0]) * gst.SECOND, vol)
		if not firstpoint:
			Globals.debug("Set extra zero fade point")
			self.volumeFadeController.set("volume", 0, 0.99)
	#_____________________________________________________________________
	
	def RemoveEventsUnderEvent(self, mainEvent, undoAction=None):
		"""
		Deletes and/or trims any events which are between the two given values.
		This is useful for ensuring that a particular section of this instrument in
		totally clear of any audio.
		
		Parameters:
			start -- the start of the interval in seconds
			stop -- the stop of the interval in seconds
		"""
		if not undoAction:
			#init our own undo action so everything is nice and atomic
			undoAction = self.project.NewAtomicUndoAction()
		
		start = mainEvent.start
		stop = mainEvent.start + max(mainEvent.duration, mainEvent.loadingLength)
		leftTrimEvent = rightTrimEvent = None
		for event in self.events:
			if event is mainEvent:
				continue
			
			eventLeft = event.start
			eventRight = event.start + event.duration
			if start <= eventLeft and eventRight <= stop:
				#this event is in between
				self.DeleteEvent(event.id, _undoAction_=undoAction)
			elif eventLeft < start < eventRight and eventRight <= stop:
				# this event is straddling the start of our interval
				leftTrimEvent = event
			elif start <= eventLeft and eventLeft < stop < eventRight:
				# this event is straddling the stop of our interval
				rightTrimEvent = event
		
		if leftTrimEvent:
			leftPiece = leftTrimEvent.SplitEvent(start - leftTrimEvent.start, _undoAction_=undoAction)
			self.DeleteEvent(leftPiece.id, _undoAction_=undoAction)
		if rightTrimEvent:
			rightTrimEvent.SplitEvent(stop - rightTrimEvent.start, _undoAction_=undoAction)
			self.DeleteEvent(rightTrimEvent.id, _undoAction_=undoAction)
	
	#_____________________________________________________________________
#=========================================================================
