#!/usr/bin/env python

import  os, sys, random, copy, getopt

class struct:
	""" A handy container for a C-like structure. Nothing here!"""



class CTable:
	offset = 0		# offset of chord into bar in ticks
	chordname = ''
	chordnotes = ()
	type = ''
	scale = ''
	chordZ = 0
	arpeggioZ = 0
	walkZ = 0
	drumZ = 0
	bassZ = 0
	scaleZ = 0

# Table of volume percentages.

vols={	'OFF':	0,	'PPPP': 20,	'PPP':	30,
	'PP':	45,	'P':	55,	'MP':	75,
	'MF':	90,	'F':	100,	'FF':	110,
	'FFF':	120,	'FFFF':	150	}


# mtrks is storage for the MIDI data as it is created. It is a dict of
# class instances. Each instance is a Mtrk(). Keys are the midi channel
# numbers. Ie, mtrks[2] is for channel 2, etc. mtrks[0] is for the meta stuff.

mtrks = {}	

# Create a class to hold all our global variables. These
# are accessed thoughout the program with `glb.seqrnd', etc.
# No functions, just variables.

class Globals:
	BperQ = 192	# midi ticks per quarter note
	tickOffset = 0	# offset of current bar in ticks
	tempo = 120	# current tempo
	barNum = 0	# current bar number
	beatNum = 0	# current beat from start of piece
	QperBar = 4 	# Beats/bar, set with TIME
	seqSize = 1	# variation sequence table size
	seqCount = 0	# running count of variation
	lineno = -1	# used for error reporting
	grooves = {}
	saveVol = {}
	libPath = ['/usr/share/mma']
	outPath = ''
	volume = vols['MF']
	mmaStart = ''
	mmaEnd = ''
	seqRnd = 0
	defaultChannelVolume = 100
		
gbl=Globals()

################################################################
# A wrapper around the standard getopt package. This permits
# any order of options / filename / options.
################################################################

class Options:
	def __init__(self, version):
	
		self.version = version
		self.debug = 0
		self.pshow = 0
		self.seqshow = 0
		self.showrun = 0
		self.noWarn = 0
		self.noOutput = 0
		self.showExpand = 0
		self.chshow = 0
		self.outfile = None
		self.infile = None
		
		args = sys.argv[1:]
		
		while args:
			args = self.parse(args)
			if args:
				if self.infile:
					self.usage("Only one input filename is permitted.")

				self.infile = args.pop(0)
		
		
	
	def parse(self, args):
	
		try:
			opts, args = getopt.getopt(args, "dpsrwnef:cv", [])
		
		except getopt.GetoptError:
			self.usage()

		for o,a in opts:
			if o == '-d':
				self.debug = 1
			elif o == '-p':
				self.pshow = 1
			elif o == '-s':
				self.seqshow = 1
			elif o == '-r':
				self.showrun = 1
			elif o == '-w':
				self.noWarn = 1
			elif o == '-n':
				self.noOutput = 1
			elif o == '-e':
				self.showExpand = 1
			elif o == '-c':
				self.chshow = 1
			elif o == '-f':
				self.outfile = a
		
			elif o == '-v':
				print "%s" % self.version
				sys.exit()

			else:
				self.usage()	  # unreachable??

		return args

	def usage(self, msg=''):
		
		print "MMA -- Musical Midi Accompaniment"
		print "     Copyright 2003, Bob van der Poel"
		print "Usage: mma [opts ...] INFILE [opts ...]"
		print "Options:"
		print "  -d  Enable LOTS of debugging messages"
		print "  -p  Display patterns as they are defined"
		print "  -s  Display sequence info during run"
		print "  -r  Display running progress"
		print "  -w  Disable warning messages"
		print "  -n  Disable generation of midi output"
		print "  -e  Show parsed/expanded lines"
		print "  -c  Display default channel assignments"
		print "  -v  Display version number"
		
		print "  -f<file>  Set output Filename"	
		
		if msg:
			print
			print msg
			
		print
		sys.exit(1)
	

###########################
# Error/warning functions #
###########################

def error(msg):
	""" Print an error message and exit.
	
	If the global line number is >=0 then print the line number
	as well.
	"""

	if gbl.lineno >=0:
		ln = "<Line %d>" % gbl.lineno
	else:
		ln = ''
		
	print "ERROR: %s %s" % (ln, msg)
	
	sys.exit()


def warning(msg):
	""" Print warning message and return. """
		

	if options.noWarn:
		return
		
	if gbl.lineno >=0:
		ln = "<Line %d>" % gbl.lineno
	else:
		ln = ''
		
	print "Warning: %s %s" % (ln, msg)
	


###############################
# Chord creation/manipulation #
###############################


def getChord( name, 

	# Table of chords. All are based on a C scale. CAUTION, if you
	# add to this table make sure there are at least 3 notes in
	# each chord! Don't make any chord longer than 8 notes! The
	# pattern define sets volumes for only 8.
	
	# This is a default arg! Makes the func much faster. 
	
	chordNotes = {
		'M':		( 0, 4, 7 ),			# C E G
		'm':		( 0, 3, 7 ),			# C Eb G
		'mb5':		( 0, 3, 6 ), 			# C Eb Gb
		'm6':		( 0, 3, 7, 9 ),			# C Eb G A
		'm7':		( 0, 3, 7, 10 ),		# C Eb G Bb
		'mM7':		( 0, 3, 7, 11 ),		# C Eb G B 
		'm(maj7)':	( 0, 3, 7, 11 ),		# same as mM7
		'm7b5':		( 0, 3, 6, 10 ),		# C Eb Gb Bb - 1/2 dim
		'7':		( 0, 4, 7, 10 ),		# C E G Bb
		'7#5':		( 0, 4, 8, 10 ),		# C E G# Bb
		'7b5':		( 0, 4, 6, 10 ),		# C E Gb Bb
		'dim':		( 0, 3, 6, 9 ),			# C Eb Gb A - Dim7
		'aug':		( 0, 4, 8 ),			# C E G#
		'+':		( 0, 4, 8 ),			# C E G#
		'6':		( 0, 4, 7, 9 ),			# C E G A
		'M7':		( 0, 4, 7, 11 ),		# C E G B
		'maj7':		( 0, 4, 7, 11 ),		# C E G B
		'sus4':		( 0, 5, 7 ),			# C F G
		'sus':		( 0, 5, 7 ),			# C F G
		'7sus':		( 0, 5, 7, 10 ),		# C F G Bb
		'7sus4':	( 0, 5, 7, 10 ),		# C F G Bb
		'sus2':		( 0, 2, 7 ),			# C D G
		'7sus2':	( 0, 2, 7, 10 ),		# C D G Bb
		'9':		( 0, 4, 7, 10, 14 ),	# C E G Bb D
		'm9':		( 0, 3, 7, 10, 14 ),	# C Eb G Bb D
		'M9':		( 0, 4, 7, 11, 14 ),	# C E G B D
		'7b9':		( 0, 4, 7, 10, 13 ),	# C E G Bb Db
		'7#9':		( 0, 4, 7, 10, 15 ),	# C E G Bb D#
		'7b5b9':	( 0, 4, 6, 10, 13 ),	# C E Gb Bb Db
		'7b5#9':	( 0, 4, 6, 10, 16 ),	# C E G Bb D#
		'7#5#9':	( 0, 4, 8, 10, 16 ),	# C E G# Bb D#
		'aug7b9':	( 0, 4, 8, 10, 13 ),	# C E G# Bb Db
		'11':		( 0, 4, 7, 10, 14, 17 ),	# C E G Bb D F
		'm11':		( 0, 3, 7, 10, 14, 17 ),	# C Eb G Bb D F
		'11b9':		( 0, 4, 7, 10, 13, 17 ),	# C E G Bb Db F
		'9#5':          ( 0, 4, 8, 10, 14 ),	# C E G# Bb D
		'9#11':		( 0, 4, 7, 10, 15, 18 ),	# C E G Bb D F#
		'M7#11':	( 0, 4, 7, 11, 15, 18 ),	# C E G B D F#
		'13':		( 0, 4, 10, 21),			# C E Bb A
		'M13':		( 0, 4, 11, 21) }			# C E Bb A
	
	, chordAdjust = {
		'A':-3,   'A#':-2,   'Ab':-4,
		'B':-1,   'B#':0,    'Bb':-2,
		'C':0,    'C#':1,    'Cb':-1,
		'D':2,    'D#':3,    'Db':1,
		'E':4,    'E#':5,    'Eb':3,
		'F':5,    'F#':6,    'Fb':4,
		'G':-5,   'G#':-4,   'Gb':-6 }
	
	):		# End of function params/header!

	""" Returns the midi values for the specified chord in a tuple.
		See the list of chord names below for the valid types.
		This has values for a C-scale. All others are dirived
		for the base of C. Easy in midi since we just need to
		add/subtract constants, based on yet another table.
	
		name - full chord name (ie. CM9, D#m7, etc).

		Returns: ( ( midi values ), chordscale, chordtype )
	
		NOTE: Chord names ARE case-sensitive!
	"""


	# Special case -- Z or z is a rest, no notes.

	if name == 'z':
		return ( (), 'z', '')		
	
	# Strip off inital degree (C, D#, etc)
	
	if chordAdjust.has_key( name[:2] ):
		scale = name[:2]
		type  = name[2:]
		
	elif chordAdjust.has_key( name[:1] ):
		scale = name[:1]
		type  = name[1:]
		
	else:
		error( "Unknown chord %s." % name )
		
	if not type:		# If no type, make it a Major
		type='M'
		
		
	if not chordNotes.has_key(type):
		error("Unknown chord type '%s' in '%s'." % (type, name) )

	ch=[]
	adj=chordAdjust[scale]
	if chordNotes[type][0] + adj < 0:
		adj += 12

	for n in chordNotes[type]:
		ch.append(n + adj )

	return ( tuple(ch), scale, type )
	
	

def invertChord(chord, n):
	""" Invert (rotate) a chord a given number of steps. 
	
	"""
	
	# No invert, just return original
	
	if n == 0:
		return chord
	
	# Rotate up. No check needed here for negative note numbers
	
	if n > 0:
		ch = list(chord[:])
		while n:
			n-=1
			ch.append(ch.pop(0)+12)
		
				
		return ch
	
	# Rotate down. Quite possible to get negative note numbers, fix them!
	
	ch = list(chord[:])
	while n:
		n+=1
		ch.insert(0, ch.pop()-12)

	f = 0
	for a in ch:
		if a < 0:
			f = 12
			break
			
	if f:
		for i in range(len(ch)):
			ch[i]+=f

	return tuple(ch)
	
	
def compressChord(chord):
	""" Compress a chord to one ocatve."""
	

	mx = chord[0] + 12		# 1st note + octave
	
	# If the last note is in range, then just return
	# skipping the copy. Last note is always the highest.

	if chord[-1] <= mx:
		return chord

	# Copy the chord from a tuple to a list and modify it
	
	ch = list(chord[:])	
	for i in range(len(ch)):
		if ch[i] > mx:
			ch[i] -= 12
			
	return tuple(ch)


###########################
# File read class
###########################


class Read:

	que = []	# que for pushed lines (mainly for REPEAT)
	qnums = []
		
	def __init__(self, fname):
	
		self.fname = fname	
	
		try:
			self.inpath = open(fname, 'r')
		except:
			error("Unable to open '%s' for input" % fname)
			
		if options.debug:
			print("File %s opened." % fname)
			
		self.lineno=0

	def close(self):
		self.inpath.close()
		if options.debug:
			print("File %s closed." % self.fname)

	def push(self, q, nums):
		""" Push a list of lines back into the input stream.
		
			Note: This is a list of semi-processed lines, no comments, etc.
		
			It's quicker to extend a list than to insert, so add to the end.
			Note: we reverse the original, extend() then reverse again, just
			in case the caller cares.
			
			nums is a list of linenumbers. Needed to report error lines.
		"""
		
		if not self.que:
			self.que = ['']
			self.qnums=[self.lineno]
			
		q.reverse()
		self.que.extend(q)
		q.reverse()
		
		nums.reverse()
		self.qnums.extend(nums)
		nums.reverse()
		
	def read(self):
		while 1:
			if self.que:
				ln = self.que.pop()
				self.lineno = self.qnums.pop()

				if not ln:
					continue
				return ln
		
			ln = self.inpath.readline()

			if not ln:		
				return None	# EOF

			self.lineno +=1			

			ln = ln.split('//')[0].strip()	# Dump comments and blank lines

			if ln:
				return ln


####################################################
# This is where the music is created.

# English names for midi instruments and drums

drumNames = {'HIGHQ':27, 'SLAP':28, 'SCRATCHPUSH':29,
		'SCRATCHPULL':30, 'STICKS':31, 'SQUARECLICK':32,
		'METRONOMECLICK':33, 'METRONOMEBELL':34, 'KICKDRUM2':35,
		'KICKDRUM1':36, 'SIDEKICK':37, 'SNAREDRUM1':38,
		'HANDCLAP':39, 'SNAREDRUM2':40, 'LOWTOM2':41,
		'CLOSEDHIHAT':42, 'LOWTOM1':43, 'PEDALHIHAT':44,
		'MIDTOM2':45, 'OPENHIHAT':46, 'MIDTOM1':47, 'HIGHTOM2':48,
		'CRASHCYMBAL1':49, 'HIGHTOM1':50, 'RIDECYMBAL1':51,
		'CHINESECYMBAL':52, 'RIDEBELL':53, 'TAMBOURINE':54,
		'SPLASHCYMBAL':55, 'COWBELL':56, 'CRASHCYMBAL2':57,
		'VIBRASLAP':58, 'RIDECYMBAL2':59, 'HIGHBONGO':60,
		'LOWBONGO':61, 'MUTEHIGHCONGA':62, 'OPENHIGHCONGA':63,
		'LOWCONGA':64, 'HIGHTIMBALE':65, 'LOWTIMBALE':66,
		'HIGHAGOGO':67, 'LOWAGOGO':68, 'CABASA':69, 'MARACAS':70,
		'SHORTHIWHISTLE':71, 'LONGLOWWHISTLE':72, 'SHORTGUIRO':73,
		'LONGGUIRO':74, 'CLAVES':75, 'HIGHWOODBLOCK':76,
		'LOWWOODBLOCK':77, 'MUTECUICA':78, 'OPENCUICA':79,
		'MUTETRIANGLE':80, 'OPENTRIANGLE':81, 'SHAKER':82,
		'JINGLEBELL':83, 'CASTANETS':85, 'MUTESUDRO':86,
		'OPENSUDRO':87} 


voiceNames = {'PIANO1':0, 'PIANO2':1, 'PIANO3':2,
		'HONKY-TONKPIANO':3, 'RHODESPIANO':4, 'EPIANO':5,
		'HARPSICHORD':6, 'CLAVINET':7, 'CELESTA':8, 'GLOCKENSPIEL':9,
		'MUSICBOX':10, 'VIBRAPHONE':11, 'MARIMBA':12, 'XYLOPHONE':13,
		'TUBULARBELLS':14, 'SANTUR':15, 'ORGAN1':16, 'ORGAN2':17,
		'ORGAN3':18, 'CHURCHORGAN':19, 'REEDORGAN':20,
		'ACCORDION':21, 'HARMONICA':22, 'BANDONEON':23,
		'NYLONGUITAR':24, 'STEELGUITAR':25, 'JAZZGUITAR':26,
		'CLEANGUITAR':27, 'MUTEDGUITAR':28, 'OVERDRIVEGUITAR':29,
		'DISTORTONGUITAR':30, 'GUITARHARMONICS':31, 'ACOUSTICBASS':32,
		'FINGEREDBASS':33, 'PICKEDBASS':34, 'FRETLESSBASS':35,
		'SLAPBASS1':36, 'SLAPBASS2':37, 'SYNTHBASS1':38,
		'SYNTHBASS2':39, 'VIOLIN':40, 'VIOLA':41, 'CELLO':42,
		'CONTRABASS':43, 'TREMOLOSTRINGS':44, 'PIZZICATOSTRING':45,
		'ORCHESTRALHARP':46, 'TIMPANI':47, 'STRINGS':48,
		'SLOWSTRINGS':49, 'SYNTHSTRINGS1':50, 'SYNTHSTRINGS2':51,
		'CHOIRAAHS':52, 'VOICEOOHS':53, 'SYNTHVOX':54,
		'ORCHESTRAHIT':55, 'TRUMPET':56, 'TROMBONE':57, 'TUBA':58,
		'MUTEDTRUMPET':59, 'FRENCHHORN':60, 'BRASSSECTION':61,
		'SYNTHBRASS1':62, 'SYNTHBRASS2':63, 'SOPRANOSAX':64,
		'ALTOSAX':65, 'TENORSAX':66, 'BARITONESAX':67, 'OBOE':68,
		'ENGLISHHORN':69, 'BASSOON':70, 'CLARINET':71, 'PICCOLO':72,
		'FLUTE':73, 'RECORDER':74, 'PANFLUTE':75, 'BOTTLEBLOW':76,
		'SHAKUHACHI':77, 'WHISTLE':78, 'OCARINA':79, 'SQUAREWAVE':80,
		'SAWWAVE':81, 'SYNCALLIOPE':82, 'CHIFFERLEAD':83,
		'CHARANG':84, 'SOLOVOICE':85, '5THSAWWAVE':86, 'BASS&LEAD':87,
		'FANTASIA':88, 'WARMPAD':89, 'POLYSYNTH':90, 'SPACEVOICE':91,
		'BOWEDGLASS':92, 'METALPAD':93, 'HALOPAD':94, 'SWEEPPAD':95,
		'ICERAIN':96, 'SOUNDTRACK':97, 'CRYSTAL':98, 'ATMOSPHERE':99,
		'BRIGHTNESS':100, 'GOBLINS':101, 'ECHODROPS':102,
		'STARTHEME':103, 'SITAR':104, 'BANJO':105, 'SHAMISEN':106,
		'KOTO':107, 'KALIMBA':108, 'BAGPIPE':109, 'FIDDLE':110,
		'SHANAI':111, 'TINKLEBELL':112, 'AGOGOBELLS':113,
		'STEELDRUMS':114, 'WOODBLOCK':115, 'TAIKODRUM':116,
		'MELODICTOM1':117, 'SYNTHDRUM':118, 'REVERSECYMBAL':119,
		'GUITARFRETNOISE':120, 'BREATHNOISE':121, 'SEASHORE':122,
		'BIRDTWEET':123, 'TELEPHONERING':124, 'HELICOPTERBLADE':125,
		'APPLAUSE/NOISE':126, 'GUNSHOT':127 }
		
	
class Pats:
	""" Pattern class.
	
		Define classes for processing drum, chord, arp, and chord track.
		These are mostly the same, so we create a base class and derive
		the others from it.
	
	"""
	
	def __init__(self, nm):
		
		self.name = nm	
		self.channel = 0
		self.pats = {}
		self.grooves = {}
		self.saveVols = {}
		self.transpose = 0	
		self.invert = (0,)	# inverts match sequence
		self.voice = (0,)	 
		self.ssvoice = -1	# Track the voice set for the track
		self.artic = (90,)	# Note length clip adjustment
				
		self.disable = 0
		
		self.clearSequence()
		
		if self.name == 'DRUM':
			self.setChannel(10)
			
		if self.vtype == 'DRUM':
			self.channel = 10
			self.ssvoice = 0
	
					
	##########################################
	## These are called from process() to set options

	def setCompress(self, ln):
		""" set/unset the compress flag. """
	
		ln = self.lnExpand(ln, 'Compress')

		for i in range(len(ln)):
			
			n = stoi(ln[i], "Argument for %s Compress must be a value." \
					% self.name)
					
			if n < 0 or n > 5:
				error("Compress %s out-of-range; must be 0 to 5." % n)
						
			ln[i] = n
			
		self.compress = tuple( ln )
		
		if options.debug:
			print "Set %s Compress to " % self.name,
			printList(ln)
		
		
	def setChannel(self, ln):
		""" Set the midi-channel number for a track. 
		
			Checks for channel duplication
			Sets default channel volume.
		"""
	
		c = stoi(ln, "%s Channel assignment expecting Value, not %s" %
			(self.name, ln))

		if c<0 or c>16:
			error("%s Channel must be 0..16, not %s" % (self.name, ln))
			
		if self.vtype == 'DRUM' and c != 10:
			error("MIDI Channel for DRUM tracks cannot be reassigned.")
			
		if c == 10 and self.vtype != 'DRUM':
			error("Channel 10 is reserved for DRUM, not %s." % self.name)
		
		old = self.channel
		
		# Disable the channel. This means anything written to this
		# MMA track will cause an error. But, you can now reuse the old channel

		if c == 0:
			self.channel = 0
			warning("%s channel disabled, %s available." %
				(self.name, old ))
			return
			
			
		for a in tnames.keys():
			if a == self.name:	# okay to reassign same number
				continue
				
			if tnames[a].channel == c:
				error("%s is assigned to %s." % (c, tnames[a].name ) )
				
		self.channel = c

		if not mtrks.has_key(c):
			mtrks[c]=Mtrk(c)
			
			mtrks[c].addTrkName(0,self.name.title())
			self.setChannelVolume(gbl.defaultChannelVolume)
	

	def setChShare(self, ln):
		""" Share midi-channel setting. """
		
		sc = ln.upper()
		
		if not tnames.has_key(sc):
			error("Channel '%s' does not exist. No such name." % sc)
		
		if sc == self.name:
			error("%s can't share MIDI channel with itself." % sc)
			
		self.channel = tnames[sc].channel
		
		if self.channel == 0:
			warning("Channel %s MIDI channel is currently disabled." % sc)

		else:
			warning("%s and %s now share MIDI channel %s" %
				(sc, self.name, self.channel))
			
				
	def setChannelVolume(self, v):
		""" LowLevel MIDI command. Set Channel Voice. """
		
		if not self.channel:
			error("MIDI channel has not been assigned for %s" % self.name)
			
		mtrks[self.channel].addChannelVol(gbl.tickOffset, v)
			
		if options.debug:
			print "%s ChannelVolume set to %s" % (self.name, v)

				
	def setStrum(self, ln):
		""" Set Strum time. This is a error trap for non-chord."""
		
		error( "Strum is only valid in Chord tracks, you tried to " 
			"set it in a %s track." % self.name)


	def setTone(self, ln):
		""" Set Tone. Error trap, only drum tracks have tone. """
		
		error("Tone command not supported for %s track." % self.name)

		
	def setOn(self):
		""" Turn ON track. """
		
		self.disable = 0

		if options.debug:
			print "%s Enabled" % self.name


		
	def setOff(self):
		""" Turn OFF track. """
		
		self.disable = 1

		if options.debug:
			print "%s Disabled" % self.name

	

	def setGlis(self, v):
		""" Set the portamento. LowLevel MIDI. """
		
		if v<0 or v>127:
			error("Value for Portamento must be 0..127")

		mtrks[self.channel].addGlis(gbl.tickOffset, v)

		if options.debug:
			print "%s Glis set to %s" % (self.name, v)



	def setRVolume(self, ln):
		""" Set the volume randomizer for a track. """

		ln = self.lnExpand(ln, 'RVolume')

		for i in range(len(ln)):
			
			n = stoi(ln[i], "Argument for %s RVolume must be a value." \
					% self.name)
					
			if n < 0 or n > 100:
				error("RVolume %s out-of-range; must be 0..100." % n)
						
			if n > 30:
				warning("%s is a large RVolume value!" % n)
					
			ln[i] = n

			
		self.rVolume = tuple( ln )
			
		if options.debug:
			print "Set %s Rvolume to " % self.name,
			printList(ln)


	def setRSkip(self, ln):
		""" Set the note random skip factor for a track. """

		ln = self.lnExpand(ln, 'RSkip')

		for i in range(len(ln)):

			n = stoi(ln[i], "Expecting integer after in RSkip")
			
			if n < 0 or n > 99:
				error("RSkip arg must be 0..99")
			
			ln[i] = n 

		self.rSkip = tuple( ln )
			
		if options.debug:
			print "Set %s RSkip to " % self.name,
			printList(ln)



	def setRTime(self, ln):
		""" Set the timing randomizer for a track. """
		
		ln=self.lnExpand(ln,  'RTime')
		
		for i in range(len(ln)):
			n=stoi(ln[i], "Expecting an integer for Rtime")
			if n < 0 or n > 100:
				error("RTime %s out-of-range; must be 0..100." % n)
			
			ln[i]=n
				
		self.rTime = tuple( ln )

		if options.debug:
			print "Set %s RTime to " % self.name,
			printList(ln)

		
	def setRnd(self):
		""" Enable random pattern selection from sequence."""
		
		self.seqRnd = 1
		if options.debug:
			print "Set %s SeqRnd." % self.name

		
	def setNoRnd(self):
		""" Disable random pattern selection from sequence."""

		self.seqRnd = 0
		if options.debug:
			print "Cleared %s SeqRnd." % self.name


	def setDirection(self, ln):
		""" Set scale direction. """
		
		ln = self.lnExpand(ln, "Direction")
		
		vald = ('UP', 'DOWN', 'BOTH', 'RANDOM')
		for i in range(len(ln)):
			n = ln[i].upper()
			if not n in vald:
				error("Unknown %s Direction. Only '%s' are valid." \
					% (self.name, ' '.join(vald) ))
			ln[i]=n
			
		self.direction = tuple( ln )

		if options.debug:
			print "Set %s Direction to " % self.name,
			printList(ln)


	def setScaletype(self, ln):
		""" Set scale type. """
		
		ln = self.lnExpand(ln, "ScaleType")
		
		vald = ('MAJOR', 'MINOR', 'CHROMATIC', 'AUTO')
		for i in range(len(ln)):
			n = ln[i].upper()
			if not n in vald:
				error("Unknown %s ScaleType. Only '%s' are valid." \
					% (self.name, ' '.join(vald) ))
			ln[i]=n
			
		self.scaleType = tuple( ln )

		if options.debug:
			print "Set %s ScaleType to " % self.name,
			printList(ln)

	
	def setInvert(self, ln):
		""" Set inversion for track.
			
			This can be applied to any track,
			but has no effect in drum tracks. It inverts the chord
			by one rotation for each value.
		"""
		
		ln=self.lnExpand(ln, "Invert")
		
		for i in range(len(ln)):
			ln[i] = stoi(ln[i], "Argument for %s Invert must be an integer" \
					% self.name)
		
		self.invert = tuple( ln )

		if options.debug:
			print "Set %s Invert to " % self.name,
			printList(ln)

							
	def setOctave(self, ln):
		""" Set the octave for a track. """
		
		ln=self.lnExpand(ln, 'Octave')
		
		for i in range(len(ln)):
			n = stoi(ln[i], "Argument for %s Octave must be an integer" \
					% self.name)
			if n < 0 or n > 10:
				error("Octave %s out-of-range; must be 0..10." % n)

			ln[i] = n * 12
		
		self.octave = tuple( ln )
		
		if options.debug:
			print "Set %s Octave to" % self.name,
			for i in self.octave:
				print i/12,
			print


	def setTranspose(self, t):
		""" Set the transpostione for a track.
		
			This will be called for all tracks. Process() doesn't
			have a point for transpose to be applied to only one track.
			No effect on drum tracks.
		"""
		
		if t < -12 or t > 12:
			error("Tranpose %s out-of-range; must be -12..12." % t)
			
		self.transpose=t
		
		if options.debug:
			print ("Set %s Transpose to %s" % (self.name, self.transpose))

	
	def setSeqSize(self, ssize):
		""" Expand existing pattern list. """
		
		def bump(l):
			while len(l) < ssize:
				l += l
			return l
			
		if len(self.sequence):
			self.sequence = bump(self.sequence)
		
		self.invert    = bump(self.invert)
		self.artic     = bump(self.artic)
		self.volume    = bump(self.volume)
		self.voice     = bump(self.voice)
		self.rVolume   = bump(self.rVolume)
		self.rSkip     = bump(self.rSkip)
		self.rTime     = bump(self.rTime)
		self.strum     = bump(self.strum)
		self.octave    = bump(self.octave)
		self.direction = bump(self.direction)
		self.scaleType = bump(self.scaleType)
		self.compress  = bump(self.compress)

		if self.vtype == "DRUM":
			self.toneList = bump(self.toneList)


	def setVoice(self, ln):
		""" Set the voice for a track.
		
			Note, this just sets flags, the voice is set in bar().
			ln[] is not nesc. set to the correct length.
		"""
		
		ln=self.lnExpand(ln, 'Voice')
				
		d=voiceNames
		
		for i in range(len(ln)):
			a=ln[i].upper()	

			if d.has_key( a ):
				ln[i] = d[a]
			else:
				n=stoi(a, "Expecting a valid voice name or value, "
					"not '%s'" % a)
				if n <0 or n > 127:
					error("Voice must be 0..127.")
				ln[i]=n

		self.voice = tuple(ln)

		if options.debug:
			print "Set %s Voice to: " % self.name,
			for a in ln:
				print keyLookup(d, a),
			print


	def setVolume(self, ln):
		""" Set the volume for a pattern.
			ln - list of volume names (pp, mf, etc)
			ln[] not nesc. correct length
		"""

		ln=self.lnExpand(ln, 'Volume')
		
		d=vols
		for i in range(len(ln)):
			a=ln[i].upper()	

			if not d.has_key( a ):
				error("Expecting a valid volume, not '%s'" % ln[i])
			ln[i] = d[a]
			
		self.volume = tuple(ln)
			
		if options.debug:
			print "Set %s Volume to " % self.name,
			for a in ln:
				print keyLookup(d,a),
			print


	def setArtic(self, ln):
		""" Set the note articuation value. """
	
		ln=self.lnExpand(ln, 'Articulate')
				
		for i in range(len(ln)):
			a = stoi(ln[i], "Expecting value in articulation setting.")
			if a < 1 or a > 100:
				error("Articulation setting must be 1..100, "
					"not %s." % a)
			
			if a == 100:
				warning("Articulation of 100 for %s might " 
					"cause problems." % self.name )

			ln[i] = a
			
		self.artic = tuple(ln)
			
		if options.debug:
			print "Set %s Articulation to " % self.name,
			printList(ln)


	
	def lnExpand(self, ln, cmd):
		""" Validate and expand a list passed to a set command. """
		
		if len(ln) > gbl.seqSize:
			warning("%s list truncated to %s patterns." % 
				(self.name, gbl.seqSize) )
			
		while len(ln) < gbl.seqSize:
			ln += ln
		
		if ln[0] == '/':
			error ("You cannot use a '/' as the first item in a %s list." 
				% cmd)
	
		for i in range(len(ln)):
			if ln[i]=='/':
				ln[i]=ln[i-1]
				
		return ln

	


	##################################################
	## Save/restore grooves
						
	def saveGroove(self, gname):
		""" Define a groove.
		
			Called by the 'DefGroove Name'. This is called for
			each track. 
		
			If 'gname' is already defined it is overwritten.	
		
			Note aux. function which may be defined for each track type.
		"""
		
		self.grooves[gname] = {
			'PATS':    self.pats.copy(),	
			'SEQ':     self.sequence[:],
			'VOLUME':  self.volume[:],
			'RVOLUME': self.rVolume[:],
			'RSKIP':   self.rSkip[:],
			'RTIME':   self.rTime[:],
			'STRUM':   self.strum[:],
			'OCTAVE':  self.octave[:],
			'DIR':     self.direction[:],
			'SCALE':   self.scaleType[:],
			'VOICE':   self.voice[:],
			'INVERT':  self.invert[:],
			'ARTIC':   self.artic[:],
			'SEQRND':  self.seqRnd,
			'COMPRESS':self.compress[:],
			'CHAN':    self.channel }
	
		if self.vtype == 'DRUM':
			self.grooves[gname]['TONES'] = self.toneList[:]
		
				
	def restoreGroove(self, gname):
		""" Restore a defined groove. """
		

		if not self.grooves.has_key(gname):
			error("Goove name '%s' not defined" % gname)
		
		g = self.grooves[gname]
		
		self.sequence  =  g['SEQ']
		self.volume    =  g['VOLUME']
		self.voice     =  g['VOICE']
		self.rTime     =  g['RTIME']
		self.rVolume   =  g['RVOLUME']
		self.rSkip     =  g['RSKIP']
		self.strum     =  g['STRUM']
		self.octave    =  g['OCTAVE']
		self.direction =  g['DIR']
		self.scaleType =  g['SCALE']
		self.invert    =  g['INVERT']
		self.artic     =  g['ARTIC']
		self.seqRnd    =  g['SEQRND' ]
		self.channel   =  g['CHAN' ]
		self.compress  =  g['COMPRESS']


		if self.vtype == 'DRUM':
			self.toneList = g['TONES']	
				
		# This loop  is necessary. If we just restore self.pats{} 
		# from the saved version, new defs will be lost.
		# Don't ask how I know this.

		for p in g['PATS'].keys():
			self.pats[p]=g['PATS'][p]
		
		
		# We might have changed the channel, so set the need voice
		# setting. At the worst, this will insert some extra voice parms.
		# Our little test might screw up drum voice changes, but they
		# probably aren't being done.
		
				
		if self.vtype != 'DRUM':
			self.ssvoice = -1
		
		
 
	def defVolume(self, name):
		""" Save current volume setting to a name.
		
			Called by the 'SetVolume Name'. This is called for
			each track. 
			
			If 'name' is already defined it is overwritten.	
		"""
			
		self.saveVols[name] = self.volume[:]
		

	def restoreVolume(self, name):
		""" Restore a saved volume. """
		
		if not self.saveVols.has_key(name):
			error("Volume name '%s' not defined" % name)
			
		self.volume=self.saveVols[name]
		
			
	####################################
	## Sequence functions
	
	def setSequence(self, ln):
		""" Set the sequence for a track.
		
		names - a list of pattern names.
			The names do not have to be defined (yet).
			
			Existing sequences are silently overwritten.
		"""

		ln=self.lnExpand(ln, 'Sequence')
		
		if ln[0] == '-':
			ln=[]
			
		else:
			for i in range(len(ln)):	
				ln[i]=ln[i].upper()
		
		self.sequence = tuple(ln)
		
		if options.seqshow:
			print "%s sequence set:" % self.name,
			for a in self.sequence:
				print a,
			print

		
	def clearSequence(self):
		""" Clear sequence for track.
			This is also called from __init__() to set the 
			initial defaults for each track.
		"""
		
		self.sequence   =  ()
		self.seqRnd     =  0
		self.octave     =  (4 * 12,)
		self.scaleType  =  ('AUTO',)
		self.rVolume    =  (0,)
		self.rSkip      =  (0,)
		self.rTime      =  (0,)
		self.strum      =  (0,)		
		self.volume     =  (vols['MF'],)
		self.compress   =  (0,)
			
		if self.vtype == 'CHORD':
			self.direction = ('UP',)
		else:
			self.direction  =  ('BOTH',)
				
		self.setSeqSize(gbl.seqSize)


	############################
	### Pattern functions
	############################
	
	

	def definePattern(self, name, ln):
		""" Define a Pattern. """

		def patsort(c1, c2):
			""" Sort a pattern tuple. """
		
			if c1.offset < c2.offset: return -1
			if c1.offset == c2.offset: return 0
			else: return 1
	
				
		# Patterns are shared for all tracks of the same type. 
		# We store and retrieve from the base track, so use
		# the track type which should be same. Ie: track DRUM3
		# has a type name of DRUM.
		
		slot = self.vtype	
		
		# Convert "1 2 3; 4 5 6" to  [ [1,2,3], [4,5,6] ]

		p = []
		ln = ln.split(';')
		for l in ln:
			p.append(l.split())
	
		plist=[]
		pats=tnames[slot].pats
		
		
		for ev in p:
			
			if len(ev) == 1:
				nm = ev[0].upper()
				if pats.has_key(nm):
					plist.extend(pats[nm])
					continue
				else:
					error("%s is not an existing %s pattern." \
						% (nm, self.vtype.title()) )
					
			plist.append( self.getPgroup(ev))
		

		if pats.has_key(name):
			redef = 'REDEFINED'
		else:
			redef = 'created'
		
		plist.sort(patsort)
		pats[name]=tuple(plist)
				
		if options.pshow:
			print "%s pattern %s %s:" % (slot.title(), name, redef )
			self.printPattern(plist)

	
	def mulPattern(self, newpat, oldpat, fact):
		"""Create a new pattern by  multiplying an existing pattern.
		
		This can be used to take a pattern of 4 1/4 notes and turn
		it into a pattern of 8 1/8 notes.
		"""
		
		slot = self.vtype
		pats = tnames[slot].pats
		
		fact = stoi(fact, "The multiplier arg must be an integer")
		
		if fact<1 or fact >100:
			error("The multiplier arg must be in the range 2 to 100.")

		oldpat = oldpat.upper()
		newpat = newpat.upper()
		
		if not pats.has_key(oldpat):
			error("%s pattern '%s' must be defined before we can multipy it"
				% (slot, oldpat))
		
		# Make N copies of pattern, adjusted so that the new copy has all
		# note lengths and start times  adjusted.
		# eg: [[1, 2, 66], [3, 2, 88]]  * 2
		# becomes [[1,4,66], [2,4,88], [3,4,66], [4,4,88]].
				
		new = []
		add = 0
		step = (gbl.BperQ * gbl.QperBar)/fact


		for n in range(fact):
			orig = copy.deepcopy(pats[oldpat])
			for z in orig:
				z.offset = (z.offset / fact) + add
				z.duration /= fact
				if z.duration < 1:
					z.duration = 1
	
				new.append(z)
			add += step
		
		
		if pats.has_key(newpat):
			redef = 'REDEFINED'
		else:
			redef = 'created'

		pats[newpat]= new	# Don't need sort!

		if options.pshow:
			print "%s pattern %s %s as %s * %s:" \
				% (slot, newpat, redef, fact, oldpat)
			self.printPattern(new)
		
		
	def shiftPattern(self, newpat, oldpat, fact):
		"""Create a new pattern by  shifting an existing pattern. """
		
		slot = self.vtype
		pats = tnames[slot].pats
		
		fact = stof(fact, "The multiplier arg must be an value")
		
		oldpat = oldpat.upper()
		newpat = newpat.upper()
		
		if not pats.has_key(oldpat):
			error("%s pattern '%s' must be defined before we can shift it"
				% (slot, oldpat))
		
		# Adjust all the beat offsets
		
		new = copy.deepcopy(pats[oldpat])				
		max = gbl.BperQ * (gbl.QperBar)
		for n in new:
			n.offset += fact * gbl.BperQ
			if n.offset < 0 or n.offset > max:
				error("Pattern shift with factor %f has resulted in an "
					"illegal offset." % fact )
		
		if pats.has_key(newpat):
			redef = 'REDEFINED'
		else:
			redef = 'created'
		
		pats[newpat]= new	# Don't need sort!

		if options.pshow:
			print "%s pattern %s %s as %s Shift %s:" \
				% (slot, newpat, redef, oldpat, fact)
			self.printPattern(new)
			
					
	def printPattern(self, pat):
		""" Print a pattern. Used by debugging code."""
		
		s=[]
		for p in pat:
			s.append(" %2.2f %2.0f" % (1+(p.offset/float(gbl.BperQ)),
				p.duration))
				
			if self.vtype == 'CHORD':
				for a in p.vol:
					s.append( " %2.0f" % a)

			elif self.vtype == 'BASS':
				s.append( " %2.0f %2.0f" % (p.noteoffset, p.vol ) )
			
			elif self.vtype == 'APRPEGGIO':
				s.append( " %2.0f " % p.vol )
				
			elif self.vtype == 'DRUM':
				s.append(" %2.0f" %  p.vol)
			
			elif self.vtype == 'WALK':
				s.append(" %2.0f" % p.vol )
				
			s.append(' ;')
			s.append('\n')
		s[-2]=' @'
		print "".join(s)

	
	#########################
	## Music processing
	#########################
		
	def bar(self, ctable):
		""" Process a bar of music. """

		
		# Not having a sequence defined for a track is fine.
		# Just don't do anything for it...
		
		
		if not self.sequence or self.disable:
			return

		if not self.channel:
			error("MIDI channel has not been assigned for %s" % self.name)
		

		# We only permit patterns to be defined for tracks NOT ending
		# in a digit (ie DRUM can have a pattern, DRUM1 can't).
		# So, all the Drum patterns are shared for all the DRUM tracks.
		# We use the 'type' as the name for this track.

		pats=tnames[self.vtype].pats			
		
		# Decide which seq to use. This is either the current
		# seqCount, or if SeqRnd has been set for the track
		# it is a random pattern in the sequence. 

		# The class variable self.seq is set to the sequence to
		# use. 
		
		
		if self.seqRnd:
			self.seq = random.randrange(gbl.seqSize)
		else:
			self.seq = gbl.seqCount
			
		
		# The special pattern 'Z' is reserved. It means do nothing...
		
		sc = self.seq
		
		if self.sequence[sc] == 'Z' or self.sequence[sc]=='':
			return

		v = self.voice[sc]
			
		if v != self.ssvoice:
			mtrks[self.channel].addProgChange( gbl.tickOffset,  v)
			self.ssvoice = v
			
			if options.debug:
				print "Track %s voice %s inserted." \
					% (self.name, keyLookup(voiceNames,v))


		
		
			
		try:
			pattern = pats[self.sequence[sc]]

		except:
			error("%s Pattern %s is not defined" %
				(self.name, self.sequence[sc]))
		
		self.trackBar(pattern, ctable)

	
		
	def getChordInPos( self, off, ctable):
		""" Compare an offset to a list of ctables and return
			the table entry active for the given beat. 
			
			We assume that the first offset in 'ctable' is 0!
			We assme that 'off' is >= 0!
	
			Returns a ctable.
		"""
		
		for i in range(len(ctable)-1, -1, -1):	# reverse order
			if off >= ctable[i].offset:
				break
		return ctable[i]



	def adjustVolume(self, v):
		""" Adjust a note volume based on the track and global volume setting. """

		if not v:
			return 0
		
		sc = self.seq
		
		if self.rSkip[sc] and random.random() * 100 < self.rSkip[sc]:
			return 0

		v =  (v * self.volume[sc] * gbl.volume)	/ 10000

		if self.rVolume[sc]:
			a = (v * self.rVolume[sc])/100
			if a:
				v += random.randrange(-a, a)
				
		if v > 127:
			v = 127
		elif  v < 1:
			v = 1
			
		return v
		
			
	def adjustNote(self, n):
		""" Adjust a note for a given octave/transposition.
			Ensure that the note is in range.
		"""
	
		n += self.octave[self.seq] + self.transpose
		
		while n < 0:
			n += 12
		while n > 127:
			n -= 12
		
		return n

	
	def setBarOffset(self, v):
		""" Convert a string into a valid bar offset in midi ticks. """

		v=stof(v, "Value for %s bar offset must be integer/float" % self.name)	
		
		if v < 1:
			error("Defining %s Pattern, bar offset must be 1 or greater." %
				 self.name)
				 
		if v >= gbl.QperBar+1:
			error("Defining %s Pattern, bar offset must be less than %s." %
				(self.name, gbl.QperBar + 1))
				
				
		return int((v-1) * gbl.BperQ)
		
		
	def getDur(self, d):
		""" Return the adjusted duration for a note. 
		
			The adjustment makes notes more staccato. Valid
			adjustments are 1 to 100. 100 is not recommended.
		"""

		d = (d * self.artic[self.seq]) / 100
		if not d:
			d = 1
			
		return d
	
	
##############################################################
# We have a class for each track type. They are all derived
# from the base 'Pats' class. These classes do special processing
# like parsing a pattern tuple and creating a chord.
##############################################################
	
	
class Bass(Pats):
	""" Pattern class for a bass track. """

	vtype = 'BASS'	
			
	def getPgroup(self, ev):
		""" Get group for bass pattern.
				
			Tuples:  [start, length, note, volume]
		
		"""
		
		a = struct()
		if len(ev) != 4:	
			error("There must be n groups of 4 in a pattern definition.")
			
		a.noteoffset = stoi(ev[2], "Note offset in Bass definition not int.")
		a.vol = stoi(ev[3], "Note volume in Bass definition not int.")
		
		a.duration = getNoteLen( ev[1] )

		a.offset = self.setBarOffset(ev[0])
					
		if a.noteoffset<0 or a.noteoffset>3:
			error("Chord offsets must be 0..3.")
		
		return a


	def trackBar(self, pattern, ctable):
		""" Do the bass bar.
		
		Called from self.bar()
		
		"""
		
		sc = self.seq
		
		for p in pattern:
			ct = self.getChordInPos(p.offset, ctable)
			
			if ct.bassZ:
				continue
			
			chord=ct.chordnotes
			
			if self.compress[sc]:
				chord = compressChord(chord)
				
			if self.invert[sc]:
				chord = invertChord(chord, self.invert[sc])

			n = self.adjustNote(chord[p.noteoffset])
			
			v=self.adjustVolume(p.vol)
			
			if not v:
				continue
			
			mtrks[self.channel].addPairToTrack(p.offset, self.rTime[sc],
				self.getDur(p.duration), n, v )
	

class Chord(Pats):
	""" Pattern class for a chord track. """

	vtype = 'CHORD'
	
	def setStrum(self, ln):
		""" Set the Stum for a chord track."""
		
		ln=self.lnExpand(ln, 'Strum')
		
		for i in range(len(ln)):
			n = stoi(ln[i], "Argument for %s Strum must be an integer" \
					% self.name)
		
			if n < 0 or n > 100:
				error("Strum %s out-of-range; must be 0..100." % n)
			
			ln[i] = n
			
		self.strum = ln
		
		if options.debug:
			print "Set %s Strum to %s" % (self.name, self.strum)

		
	def getPgroup(self, ev):
		""" Get group for chord pattern.
		
			Tuples: [start, length, volume (,volume ...) ]
		"""
			
		a = struct()
		if len(ev) < 3:	
			error("There must be at least 3 items in each group "
				"of a chord pattern definition.")
		
		a.vol = []
		for i in ev[2:]:
			a.vol.append(stoi(i,
				"Expecting integer in vols for Chord definition."))

		while(len(a.vol)<8):	# force 8 volumes
			a.vol.append(a.vol[-1])
		
		a.duration = getNoteLen(ev[1])	
							
		a.offset = self.setBarOffset(ev[0])
			
		return a
		
	
	def trackBar(self, pattern, ctable):
		""" Do a chord bar.
		
		Called from self.bar()
		
		"""
			

		sc = self.seq
		
		for p in pattern:
			tb = self.getChordInPos(p.offset, ctable)

			if tb.chordZ:
				continue
			
			chord = tb.chordnotes
			
			# Compress chord into single octave if 'compress' is set
			# We do it here, before octave, transpose and invert!
			
			if self.compress[sc]:
				chord = compressChord(chord)

			if self.invert[sc]:
				chord = invertChord(chord, self.invert[sc])

			strumAdjust = self.strum[sc] 
			strumOffset = 0
			if strumAdjust and self.direction[sc]=='DOWN':
				strumOffset += strumAdjust * len(chord)
				strumAdjust = -strumAdjust
				
			for n,v in zip(chord, p.vol):
				v = self.adjustVolume( v )
				if not v:
					continue

				n = self.adjustNote(n)

				mtrks[self.channel].addPairToTrack(p.offset+strumOffset,
					 self.rTime[sc],
					 self.getDur(p.duration), n, v )
				strumOffset += strumAdjust
				



class Arpeggio(Pats):
	""" Pattern class for an arpeggio track. """

	vtype = 'ARPEGGIO'
	arpOffset=0
	arpDirection=1
	lastDirection=1

	def getPgroup(self, ev):
		""" Get group for apreggio pattern.
		
			Tuples: [start, length, volume]
		"""
	
		a = struct()
		if len(ev) != 3:	
			error("There must be at exactly 3 items in "
				"each group for an apreggio define.")
				
		a.vol = stoi(ev[2], "Type error in Arpeggio definition.")
		
		a.duration = getNoteLen(ev[1])
				
		a.offset = self.setBarOffset(ev[0])
		
		return a
	 		

	
	def trackBar(self, pattern, ctable):
		""" Do a arpeggio bar.
		
		Called from self.bar()
		
		"""
		
		sc = self.seq
		direct = self.direction[sc]
		
		if self.lastDirection != direct:
			self.lastDirection = direct
			self.arpOffset=0
		
		for p in pattern:
			
			tb = self.getChordInPos(p.offset, ctable)

			if tb.arpeggioZ:
				continue

			if direct == 'DOWN':
				self.arpDirection = -1

			chord = tb.chordnotes
			
			if self.compress[sc]:
				chord = chordCompress(chord)
				
			if self.invert[sc]:
				chord = invertChord(chord, self.invert[sc])
			
			if direct == 'BOTH':
				if self.arpOffset < 0:
					self.arpOffset = 1
					self.arpDirection = 1
				elif self.arpOffset >= len(chord):
					self.arpOffset = len(chord)-2
					self.arpDirection = -1
			
			elif direct == 'UP':
				if self.arpOffset >= len(chord) or self.arpOffset < 0:
					self.arpOffset = 0
					self.arpDirection = 1
					
			elif direct == 'DOWN':
				if self.arpOffset < 0 or self.arpOffset >= len(chord):
					self.arpOffset = len(chord)-1
					self.arpDirection = -1

			if direct == 'RANDOM':
				n = self.adjustNote( random.choice(chord) )
			else:
				n = self.adjustNote( chord[self.arpOffset] )
			
			self.arpOffset += self.arpDirection
			
			v=self.adjustVolume(p.vol)
			if not v:
				continue

			
			mtrks[self.channel].addPairToTrack(p.offset,
				self.rTime[sc],
				self.getDur(p.duration), n, v )
	
				
	
class Drum(Pats):
	""" Pattern class for a drum track. """

	vtype = 'DRUM'
	toneList = [38]

	def setTone(self, ln):
		""" Set a tone list. Only valid for DRUMs.
		    ln[] is not nesc. the right length.
		"""
		
		ln=self.lnExpand(ln, 'Tone')
		
		d=drumNames
		for i in range(len(ln)):
			a=ln[i].upper()	

			if d.has_key( a ):
				ln[i] = d[a]
			else:
				n=stoi(a, "Expecting a valid drum name or value for "
					"drum, not '%s'" % a)
				if n <0 or n > 127:
					error("Note in Drum Tone list must be 0..127.")
				ln[i] = n

		self.toneList = ln

	
	def getPgroup(self, ev):
		""" Get group for a drum pattern.
		
			Tuples: [start, length, volume]
		"""

		a = struct()

		if len(ev) != 3:	
			error("There must be at exactly 3 items in "
				"each group of a drum define.")
		
		# Volume
					
		a.vol = stoi(ev[2], "Type error in Drum volume.")

		# Duration
		
		a.duration = getNoteLen(ev[1])
		
		a.offset = self.setBarOffset(ev[0])

		return a
		

	
	def trackBar(self, pattern, ctable):
		""" Do a drum bar.
		
		Called from self.bar()
		
		"""
		
		sc = self.seq
		
		for p in pattern:
			tb = self.getChordInPos(p.offset, ctable)
			
			if tb.drumZ:
				continue

			n = self.toneList[sc]
			
			v = self.adjustVolume(p.vol)
			if not v:
				continue

			mtrks[self.channel].addPairToTrack( p.offset,
				self.rTime[sc],
				self.getDur(p.duration), n, v )
			

	
class Walk(Pats):
	""" Pattern class for a walking bass track. """

	vtype = 'WALK'
	
	lastScale = ''
	lastType = ''
	bassOffset = 0
	notes = None
	

	def getPgroup(self, ev):
		""" Get group for walking bass pattern.
		
			Tuples: [start, length, volume]
		"""

		a = struct()
		if len(ev) != 3:	
			error("There must be at exactly 3 items in each group " +\
					"in a Walking Bass definition.")
			
		a.vol = stoi(ev[2], "Type error in Walking Bass definition")

		a.duration = getNoteLen(ev[1])				
					
		a.offset = self.setBarOffset(ev[0])	
					
		return a
		

	
	def trackBar(self, pattern, ctable):
		""" Do a waling  bass bar.
		
		Called from self.bar()
		
		"""
		
		majNotes=[0,2,4,5,7,5,4,2]	# first 5 notes of a major scale
		minNotes=[0,2,3,5,7,5,3,2]	# minor
		dimNotes=[0,2,3,6,6,3,2,0]	# dim ???
		
		for p in pattern:
		
			tb = self.getChordInPos(p.offset, ctable)
			
			if tb.walkZ:
				continue
				
			root = tb.chordnotes[0]	 	# root note of chord
			type = tb.type			# M, m, 7, etc.
			scale = tb.scale		# C, Eb, etc		
			
			if scale != self.lastScale or type != self.lastType or \
					not self.notes:
				self.lastScale = scale
				self.lastType = type
				self.bassOffset = 0

				if type[0] == 'm':
					self.notes=minNotes
				elif type=='dim':
					self.notes=dimNotes
				else:
					self.notes=majNotes
				
			if self.bassOffset >= len(self.notes):
				self.bassOffset = 0
			
			# Note is determined by adding root note to note list
			
			n = self.adjustNote( root + self.notes[self.bassOffset] )

			self.bassOffset += random.choice((0,1,1,1,1,1,2,2,3,3))
			
			v=self.adjustVolume(p.vol)

			if not v:
				continue

			mtrks[self.channel].addPairToTrack( p.offset,
				self.rTime[self.seq],
				self.getDur(p.duration), n, v )
	
		

class Scale(Pats):
	""" Pattern class for a Scale track. """

	vtype = 'SCALE'
	
	lastScale = None
	lastType = None
	lastDirect = 1
	lastCompress = 0
	sOffset = 0
	notes = None
	dirfact = 1
		
	def getPgroup(self, ev):
		""" Get group for scale patterns.
		
			Tuples: [start, length, volume]
		"""

		a = struct()
		if len(ev) != 3:	
			error("There must be at exactly 3 items in each group " +\
					"in a Scale definition.")
			
		a.vol = stoi(ev[2],"Type error in Scale definition")

		a.duration = getNoteLen(ev[1])				
					
		a.offset = self.setBarOffset(ev[0])	
					
		return a
	
	def getScale(self, type, len):
		if type == 'm':
			s = [0, 2, 3, 5, 7, 8, 10, 12]	# natural minor
		elif type == 'M':
			s = [0, 2, 4, 5, 7, 9, 11, 12]	# major
		elif type == 'c':
			s = range(0,12)
		
		scale=[]
		for t in range(len):
			for n in s:
				scale.append(n + t*12)
			

		return scale
	

	def trackBar(self, pattern, ctable):
		""" Do a scale bar.
		
			Called from self.bar()
		"""
	
		sc = self.seq
		direct = self.direction[sc]

		# Set/check any difference in the compress setting between
		# this and the previous bar.

		t = self.compress[sc]
		if t != self.lastCompress:
			self.lastCompress = t
			self.notes = None
			
		if self.lastDirect != direct:
			self.lastDirect = direct
			self.notes = None

		for p in pattern:
		
			tb = self.getChordInPos(p.offset, ctable)
			
			if tb.scaleZ:
				continue
			
			root = tb.chordnotes[0]	 	# root note of chord
			type = tb.type			# M, m, 7, etc.
			scale = tb.scale		# C, Eb, etc		

				
			if scale != self.lastScale or type != self.lastType \
					 or not self.notes:
				
				self.lastScale = scale
				self.lastType = type
	
				ocount = 3
				
				if self.compress[sc]:
					ocount = self.compress[sc]

				stype = self.scaleType[sc]
				
				if stype == "CHROMATIC":
					self.notes = self.getScale('c', ocount)
				elif stype == 'MAJOR':
					self.notes = self.getScale('M', ocount)
				elif stype == 'MINOR':
					self.notes = self.getScale('m', ocount)
				else:
					if type[0] == 'm':
						self.notes=self.getScale('m', ocount)
					else:
						self.notes=self.getScale('M', ocount)

				if direct == 'DOWN':
					self.dirfact = -1
					self.sOffset = len(self.notes)-1
				else:
					self.dirfact = 1
					self.sOffset = 0


			# Keep offset into note list in range

			if self.sOffset >= len(self.notes):
				if direct == 'BOTH':
					self.dirfact = -1
					self.sOffset = len(self.notes)-2
				else:
					self.sOffset = 0
					
			elif self.sOffset < 0:
				if direct == 'BOTH':
					self.dirfact = 1
					self.sOffset = 1
				else:
					self.sOffset = len(self.notes)-1
					
			# Note is determined by adding root note to note list
			
			if direct == 'RANDOM':
				n=self.adjustNote(random.choice(self.notes))
			else:
				n = self.adjustNote( root + self.notes[self.sOffset] )
				self.sOffset += self.dirfact
			
			v=self.adjustVolume(p.vol)
			if not v:
				continue


			mtrks[self.channel].addPairToTrack( p.offset,
				self.rTime[sc],
				self.getDur(p.duration), n, v )
	
	

######################################
# Some midi number packing routines.


def intToWord(x):
	""" Convert a 2 byte MSB LSB value. """
	
	return  chr(x>>8 & 0xff) + chr(x & 0xff)

def intTo3Byte( x):
	""" Convert an int to a 3 byte MSB...LSB value. """
	
	return intToLong(x)[1:]
		
def intToLong( x):
	""" Convert an int to a 4 byte MSB...LSB value. """
	
	return intToWord(x>>16) + intToWord(x)


def intToVarNumber( x): 
	""" Convert an int to a variable length midi value. """
	
	lst = chr(x & 0x7f)
	while  1:
		x = x >> 7
		if x:
			lst = chr((x & 0x7f) | 0x80) + lst
		else:
			return lst


##########################################
# Midi2name - used for debugging.

def midi2name(n):
	while n>=12:
		n -= 12
	while n<0:
		n+=12
	a = [ 'C', 'C#', 'D','D#', 'E' , 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B' ]

	return a[n]
	

#############################################
# Midi track class. All the midi creation is done here.
# We create a class instance for each track. mtrks{}.

class Mtrk:

	def __init__(self, channel):
		self.miditrk={}
		self.channel = channel-1

	def addTimeSig(self, offset,  nn, dd, cc, bb):
		""" Create a midi time signature.
	
			delta - midi delta offset
			nn = sig numerator, beats per measure
			dd - sig denominator, 2=quarter note, 3=eighth, 
			cc - midi clocks/tick
			bb - # of 32nd notes in quarter (normally 8)
	
			returns - packed string	
		"""
	
		self.addToTrack(offset, chr(0xff) + chr(0x58) + chr(0x04) + chr(nn) +
			chr(dd) + chr(cc) + chr(bb) )


	def addText(self, offset, msg):
		""" Create a midi TextEvent."""
	
	
		self.addToTrack( offset,
			chr(0xff) + chr(0x01) + intToVarNumber(len(msg)) + msg )

		
	def addTrkName(self, offset, msg):
		""" Creates a midi track name event. """
	
		self.addToTrack(offset,
			chr(0xff) + chr(0x03) + intToVarNumber(len(msg)) + msg )
	
		
	def addProgChange( self, offset, program):
		""" Create a midi program change.
	
			program - midi program
		
			Returns - packed string
		"""
	
		self.addToTrack(offset,
			chr(0xc0 | self.channel) + chr(program) )


	def addGlis(self, offset, v):
		""" Set the portamento. LowLevel MIDI.
		
			This does 2 thing, turns portamento on/off,
			then sets the LSN rate.
		"""

		if v == 0:
			self.addToTrack(offset, 
				chr(0xb0 | self.channel) + chr(0x41) + chr(0x00) )

		else:
			self.addToTrack(offset,
				chr(0xb0 | self.channel) + chr(0x41) + chr(0x7f) )
			self.addToTrack(offset,
				chr(0xb0 | self.channel) + chr(0x05) + chr(v) )



	def addPan(self, offset, v):
		""" Set the lsb of the pan setting."""
		
		self.addToTrack(offset,
			chr(0xb0 | self.channel) + chr(0x0a) + chr(v) )

	
	def addChannelVol(self, offset, v):
		""" Set the midi channel volume."""
		
		
		self.addToTrack(offset,
			chr(0xb0 | self.channel) + chr(0x07) + chr(v) )


	def addTempo(self, offset, beats):
		""" Create a midi tempo meta event.
	
			beats - beats per second
	
			Return - packed midi string
		"""

		self.addToTrack( offset,
			chr(0xff) + chr(0x51) +chr(0x03) + intTo3Byte(60000000/beats) )


	def writeMidiTrack(self, out):

		# Create the track. We convert timing offsets to midi-deltas.
		# This is done in-memory so we can determine the size which
		# is put in before the data.
		
		tr=self.miditrk
		
		out.write( "MTrk" )
		
		sizePt = out.tell()
		out.write( intToLong(0) )	# dummy, redo at end
		
		dataPt = out.tell()
		n = tr.keys()
		n.sort()
		last = 0

		for a in n:
			delta = a-last
			if delta < 0:
				delta = 0
			for d in tr[a]:
				out.write( intToVarNumber(delta) )
				out.write( d )
				delta = 0
			last = a

		# Add an EOF to the track
		
		out.write( intToVarNumber(0))
		out.write( chr(0xff) + chr(0x2f) + chr(0x00) )
		
		totsize = out.tell() - dataPt
		out.seek(sizePt)

		out.write(intToLong(totsize))
		out.seek(0, 2)		


	

	def addPairToTrack(self, boffset, startRnd, duration, note, v):
		""" Add a note on/off pair to a track.
	
			boffset   - offset into current bar
			startRnd  - rand val start adjustment
			duration  - note len
			note      - midi value of note
			v         - midi velocity
		"""
	
		ofs = getOffset( boffset, startRnd)
		
		
		st=chr(0x90 | self.channel) + chr(note)

		self.addToTrack(ofs, st + chr(v) )
		self.addToTrack(ofs+duration, st + chr(0))
	

	def addToTrack(self, offset, event):
		""" Add an event to a track.
	
			NOTE on track composition: 
				each miditrk is a dictionary
				keys == midi timing offset
				data == list of events
			
			So, we end up with:
		
				miditrk[123] = [event1, event2, ...]
			
			Each event can be any number of bytes in length.
		
			Events are stored in creation order.
		"""
	
		tr=self.miditrk
		if tr.has_key(offset):
			tr[offset].append(event)
		else:
			tr[offset]=[event]



	

########################################
# File processing. Mostly jumps to pats
########################################

def processFile(n):
	""" Open and process a file. Errors exit. """
	
	f=Read(n)
	Process(f)
	f.close()
		

class Process:
	""" Process a mma input file. """
	
	def __init__(self, infile):
		""" Main process loop. """
		
		self.infile = infile	
		self.lastChord = ''
		self.defBegin = ''
		self.futureVol = []
				
		while 1:
			l = self.curline = infile.read()
			if l == None:
				break	
			
			gbl.lineno = infile.lineno

			if l.upper().startswith('END'):
				ln = l.split()
				if len(ln)>1:
					error("No arguments permitted for End")
					
				if not self.defBegin:
					error("No 'Begin' for 'End'")
					
				self.defBegin=None
				continue
				
			if self.defBegin:
				l = ' '.join(self.defBegin) + ' ' + l

			ln = l.split()
			
				
			action = ln[0].upper()
			
			if options.showExpand and action !='REPEAT':
				print l	
			
			
			funcs={	'TRANSPOSE':	self.transpose,
				'BEGIN':	self.begin,
				'DEFGROOVE':	self.grooveDefine,
				'GROOVE':	self.groove,
				'FERMATA':  self.fermata,
				'TIME': 	self.time,
				'SEQSIZE':	self.seqsize,
				'SEQ':		self.seq,
				'TEMPO':	self.tempo,
				'RTIME':	self.rtime,
				'RVOLUME':	self.rvolume,
				'VOLUME':	self.volume,
				'ADJUSTVOLUME': self.adjvolume,
				'CHANNELVOLUME':self.channelVol,
				'CRESC':	self.cresc,
				'DECRESC':	self.decresc,
				'DEFVOLUME':	self.defVolume,
				'SETVOLUME':	self.setVolume,
				'REPEAT':	self.repeat,
				'REPEATEND':	self.repeatend,
				'REPEATENDING': self.repeatending,	
				'INCLUDE':	self.include,
				'USE':		self.usefile,
				'MMASTART':	self.mmastart,
				'MMAEND':	self.mmaend,
				'SEQCLEAR':	self.seqClear,
				'SEQRND':	self.seqRnd,
				'SEQNORND':	self.seqNoRnd,
				'SETLIBPATH':	self.setLibPath,
				'SETOUTPATH':	self.setOutPath,
				'BEATADJUST':	self.beatAdjust,
				'PRINT':	self.lnprint,
				'COMMENT':	self.comment }

	
			if funcs.has_key(action):
				funcs[action](ln[1:])
				continue
				
			if tnames.has_key(action):	#  BASS/DRUM/APEGGIO/CHORD
		
				funcs={	'DEFINE':	self.trackDefPattern,
					'CHANNEL':	self.trackChannel,
					'CHSHARE':	self.trackChShare,
					'COMPRESS':	self.trackCompress,
					'DIRECTION':	self.trackDirection,
					'SCALETYPE':	self.trackScaletype,
					'TONE':		self.trackTone,
					'ARTICULATE':	self.trackArtic,
					'SEQUENCE':	self.trackSequence,
					'GROOVE':	self.trackGroove,
					'INVERT':	self.trackInvert,
					'RTIME':	self.trackRtime,
					'VOLUME':	self.trackVolume,
					'CRESC':	self.trackCresc,
					'DECRESC':	self.trackCresc,
					'RVOLUME':	self.trackRvolume,
					'RSKIP':	self.trackRskip,
					'CHANNELVOLUME':self.trackChannelVol,
					'OCTAVE':	self.trackOctave,
					'VOICE':	self.trackVoice,
					'STRUM':	self.trackStrum,
					'SEQCLEAR':	self.trackSeqClear,
					'SEQRND':	self.trackSeqRnd,
					'PAN':		self.trackPan,
					'SEQNORND':	self.trackSeqNoRnd,
					'OFF':		self.trackOff,
					'ON':		self.trackOn,
					'PORTAMENTO':	self.trackGlis }
	
				name = action
				if len(ln) < 2:
					error("Expecting argument after '%s'" % name)
				action = ln[1].upper()	

				if funcs.has_key(action):
					funcs[action](name, ln[2:])
				else:
					error ("Don't know '%s'" % self.curline)
					
				continue			
				
			# At this point we have to have a chord set. Anything else
			# becomes an error condition. 	

			self.mdata(ln)
			
	def transpose(self, ln):
		""" Set transpose value. """
		
		if len(ln) != 1:
			error("Use: Transpose N.")
		
		t = stoi(ln[0], "Argument for %s Tranpose must be an integer" % ln[0])

		for n in tnames:
			tnames[n].setTranspose(t)
	
	def begin(self, ln):
		if not ln:
			error("Use: BEGIN STUFF.")
			
		if self.defBegin:
			error("Can't nest BEGIN, needs and END first.")
			
		self.defBegin = ln
		
			
	def grooveDefine(self, ln):
		""" Define a groove. 
		
		Current settings are assigned to a groove name.
		"""
		
		if len(ln) != 1:
			error("Use: DefGroove  Name")
			
		slot=ln[0].upper()
		self.grooveDefineDo(slot)

		if options.debug:
			print("Groove settings saved to '%s'." % slot)
				
	def grooveDefineDo(self, slot):
	
		for n in tnames:
			tnames[n].saveGroove(slot)
	
		gbl.grooves[slot] = {'SEQSIZE': gbl.seqSize,
				'QPERBAR': gbl.QperBar,
				'SEQRND':  gbl.seqRnd }
				
	
	def groove(self, ln):
		""" Select a previously defined groove. """
		
		if len(ln) != 1:
			error("Use: Groove Name")
		
		slot=ln[0].upper()
		self.grooveDo(slot)

		if options.debug:
			print("Groove settings restored from '%s'." % slot)
		
	def grooveDo(self, slot):
		""" This is separate from groove() so we can call it from
			usefile() with a qualified name. """
			
		for n in tnames:
			tnames[n].restoreGroove(slot)
		gbl.seqCount = 0

		
		# We don't check for a valid name here...if it wasn't defined
		# it would have been caught in the .restoreGroove() function.
		
		gbl.seqSize  = gbl.grooves[slot]['SEQSIZE']
		gbl.QperBar  = gbl.grooves[slot]['QPERBAR']
		gbl.seqRnd   = gbl.grooves[slot]['SEQRND'] 

	
	
	def time(self, ln):
		""" Set the 'time sig'. """
		
		if len(ln) != 1:
			error("Use: Time N.")
			
		n = stoi(ln[0], "Argument for time must be integer.")
			
		# Nothing special about range 1..12, but we probably need
		# to have some limits? Certainly has to be >0!
				
		if n < 1 or n> 12:
			error("Time (beats/bar) must be 1..12.")
			
		# If no change, just ignore this.
		
		if gbl.QperBar != n:
			gbl.QperBar = int(n)
			
			# Time changes zap all predfined sequences
		
			for a in tnames:
				tnames[a].clearSequence()

	
	def seqsize(self, ln):
		""" Set the length of sequcences. """
		
		if len(ln) !=1:
			error("Usage 'SeqSize N'.")
					
		n = stoi(ln[0], "Argument for SeqSize must be integer.")
						
		# Setting the sequence size always resets the seq point
				
		gbl.seqCount = 0
				
		# Now set the sequence size for each track. The class call
		# will expand/contract existing patterns to match the new
		# size.
				
		gbl.seqSize = n
		for a in tnames:
			tnames[a].setSeqSize(n)
	
		if options.debug:
			print "Set SeqSize to ", n
		

	def seq(self, ln):
		""" Set the sequence point. """
		
		if len(ln) == 0:
			s = 0
		elif len(ln)==1:
			s = stoi(ln[0], "Expecting integer value after SEQ")
		else:
			error("Use: SEQ or SEQ NN to reset seq point.")
		
		
		if s > gbl.seqSize:
			error("Sequence size is '%d', you can't set to '%d'." % 
				(gbl.seqSize, s))
		
		if s==0:
			s=1
			
		if s<0:
			error("Seq parm must be greater than 0, not %s", s)
					
		gbl.seqCount = s-1
		
		gbl.seqRnd = 0
			
								
		
	def tempo(self, ln):
		""" Set tempo. """
		
		if not ln:
			error("Use: Tempo RATE [BARS].")
		
		# Parse the tempo value. If the  value is prefaced with a
		# + or - the current tempo (global) is added to the value.
		
		mul=0
		
		if ln[0][0]=='-':
			incr=-1
			v=ln[0][1:]
		elif ln[0][0]=='+':
			incr=1
			v=ln[0][1:]
		elif ln[0][0]=='*':
			mul=1
			v=ln[0][1:]
			incr=0
		else:
			incr=0
			v=ln[0]
			
		v = stof(v, "Tempo expecting value, not '%s'." % v)
		
		if mul:
			v *= gbl.tempo
			
		v=int(v)
		
		if incr:
			v *= incr
			v += gbl.tempo 
		
		# Only one arg, just set the temp to the new value and return
		
		if len(ln)==1:
			gbl.tempo = int(v)
			mtrks[0].addTempo(gbl.tickOffset, gbl.tempo) 
			if options.debug:
				print "Set Tempo to %s" % gbl.tempo
			return
		
		# Gotta have 2 args at this point
		
		
		if len(ln)!=2:
			error("Use: Tempo RATE [BARS].")
		
		# The 2nd arg is the number of bars to vary the tempo over.

		numbeats = stoi(ln[1], "Expecting value, not %s" % ln[1] )
		numbeats = int(numbeats * gbl.QperBar)
			
		if numbeats < 1:
			error("Beat count must be greater than 1.")
		
		# Vary the rate in the meta track
		
		tincr = (v - gbl.tempo) / float(numbeats)	# incr per beat
		bstart = gbl.tickOffset			# start
		boff = 0
		tempo = gbl.tempo
		for n in range(numbeats):
			tempo += tincr
			mtrks[0].addTempo(bstart + boff, int(tempo))
			boff += gbl.BperQ
		
		if tempo != v:
			mtrks[0].addTempo(bstart + boff, int(v) )
			
		if options.debug:
			print "Set future Tempo to %s over %s beats" % \
				( int(tempo), numbeats)

			
	
	def rtime(self, ln):
		""" Set random factor for note timings. """
		
		if not ln:
			error ("Use: RTime N [...].")
		
		for n in tnames:
			self.trackRtime(n, ln)
	
	def rvolume(self, ln):
		""" Set random for note volumes."""
		
		if not ln:
			error ("Use: RVolume N [...].")
			
		for n in tnames:
			self.trackRvolume(n, ln )
	

	def volume(self, ln):
		""" Set master volume. """
			
		if len(ln) != 1:
			error ("Use: Volume Dynamic [RANGE].")
		
		v=ln[0].upper()
		if not vols.has_key(v):
			error("Unknown volume '%s'." % ln[0])
		
		gbl.volume = vols[v]
		
			

	def adjvolume(self, ln):
		""" Adjust the ratio used in the volume table. """
		
		if len(ln) !=2:
			error("Use: AdjustVolume DYN RATIO.")
			
		v=ln[0].upper()
		if not vols.has_key(v):
			error("Dynamic '%s' for AdjustVolume is unknown." % ln[0] )
		
		r = stoi(ln[1], "Expecting a value for VolumeAdjust %s, not %s." % 
			(ln[0], ln[1]) )
					
		if r<0:
			error("Percentage for AdjustVolume %s must be postive, not %s." %
				(ln[0], r) )
				
		if r>180:
			warning("%s is a very large AdjustVolume percentage." % r )
			
		vols[v]=r
			
		
	def channelVol(self, ln):
		""" Set the master channel volume for all tracks."""
		
		for n in tnames:
			if tnames[n].channel:
				self.trackChannelVol(n, ln)
			
	def cresc(self, ln):
		self.fvolume( 1, ln)

	def decresc(self, ln):
		self.fvolume( -1, ln)
			
	def fvolume(self, dir, ln):
	
		if len(ln) != 2:
			error("Use: (De)Cresc Final-Dynamic Num-Bars")
			
		bcount = stoi(ln[1], "Type error in (De)Cresc, '%s'." % ln[1] )
		if bcount < 0:
			error("Bar count for (De)Cresc must be postive.")
			
		v=ln[0].upper()
		if not vols.has_key(v):
			error("Unknown volume '%s'." % ln[0] )
		
		dest = vols[v]
		
		v=gbl.volume	
		if dir > 0 and dest <= v:
			warning("Cresc volume less than current setting. " 
				" Line %s." % gbl.lineno )
				
		if dir < 0 and dest >= v:
			warning("Decresc volume greater than current setting. " 
				" Line %s." % gbl.lineno )

		step = ( dest-gbl.volume ) / bcount

		self.futureVol=[]
		
		for a in range(bcount-1):
			v += step
			self.futureVol.append(int(v))
		self.futureVol.append(dest)	
	
	def defVolume(self, ln):
		""" Save all current volume settings to a name. """
		
		if len(ln) != 1:
			error ("Use: DefVolume NAME.")
		
		slot = ln[0].upper()	
		for n in tnames:
			tnames[n].defVolume(slot)
		
		gbl.saveVol[slot] = gbl.volume
		
		if options.debug:
			print("Volumes stored to '%s'." % slot)

	
	def setVolume(self, ln):
		if len(ln) != 1:
			error ("Use: SetVolume NAME.")
			
		slot = ln[0].upper()
		for n in tnames:
			tnames[n].restoreVolume(slot)
		
		
		gbl.volume = gbl.saveVol[slot]
		
		self.futureVol = []
		
		if options.debug:
			print("Volumes restored from '%s'." % slot)


				
	def repeat(self, ln):
		""" Repeat/RepeatEnd/RepeatEnding.

			Read input until a RepeatEnd is found. The entire
			chunk is pushed back into the input stream the
			correct number of times. This accounts for endings and
			nested repeats.

		"""
		
		stack=[]
		stacknum=[]
		main=[]
		mainnum=[]
		ending = 0
		
		def repeatChunk():
			q=[]
			qnum=[]
			nesting = 0
		
			while 1:
				l=self.infile.read()
				gbl.lineno = self.infile.lineno

				if not l:
					error("EOF encountered processing Repeat")
					
				ln=l.split()
			
				act=ln[0].upper()
				
				if act=='REPEAT':
					nesting += 1
				
				elif act == 'REPEATEND' and nesting:
					nesting -= 1
			
				elif act == 'REPEATENDING' and nesting:
					pass
				
				elif act == 'REPEATEND' or act =='REPEATENDING':
					return (q, qnum, act, ln[1:])
				
				q.append(l)
				qnum.append(self.infile.lineno)

		
		main, mainnum, act, ln = repeatChunk()
		
		while 1:
			if act == 'REPEATEND':
				if ln:
					error("REPEATEND does not take an argument.")
				stack.extend(main)
				stacknum.extend(mainnum)
			
				if not ending:
					stack.extend(main)
					stacknum.extend(mainnum)

				self.infile.push(stack, stacknum)
				break
				
			elif act == 'REPEATENDING':
			
				ending = 1
				if len(ln)>1:
					error("REPEATENDING only takes one arg.")
				if len(ln)==1:
					count=stoi(ln[0], 
						"REPEATENDING takes an integer arg.")
					if count<1:
						error("REPEATENDING count must be postive.")
					if count>5:
						warning("%s is a large value for "
							"RepeatEnding" % count)
				else:
					count = 1
					
				rpt, rptnum, act, ln = repeatChunk()
				
				for c in range(count):
					stack.extend(main)
					stacknum.extend(mainnum)
					stack.extend(rpt)
					stacknum.extend(rptnum)
				
			else:
				error("Unexpected line in REPEAT")
		
	
	
	def repeatend(self, ln):
		error("Repeatend without Repeat.")
		
	def repeatending(self, ln):
		error("Repeatending without Repeat.")

			
	def include(self, ln):
		""" Include a file. """
		
		if len(ln) != 1:
			error("Use:  Include FILE" )
	
		f=Read(ln[0])
		Process( f)
		f.close()
		
		
	def usefile(self, ln):
		""" Include a library file.
			
			This saves current state and looks in all LIB dirs for
			the file.
		"""
		
		if not ln:
			error("Use: Use FILE [file..]" )

		for fn in ln:
		
			# USE saves current state, just like defining a groove.
			# Here we use a magic number which can't be created with
			# a defgroove.
		
			slot = 9988	
			self.grooveDefineDo(slot)
		
			incl=''
	
			if not gbl.libPath:
				gbl.libPath=["."]
				
			for p in gbl.libPath:
				t = os.path.expanduser("%s/%s" % (p, fn))
				if os.path.exists(t):
					incl=t
					break

			if not incl:
				error("Unable to locate include library file '%s'"\
					 % fn)
	
			# read lib file
			
			f=Read(incl)
			Process( f)
			f.close()
			
			self.grooveDo(slot)		# Restore
	
	def mmastart(self, ln):
		if len(ln) != 1:
			error ("Use: MMAstart FILE")
	
		gbl.mmaStart = ln[0]
		
	def mmaend(self, ln):
		if len(ln) != 1:
			error ("Use: MMAend FILE")
	
		gbl.mmaEnd = ln[0]
		

	def seqClear(self, ln):
		""" Clear all sequences. """
		
		if ln:
			error ("Use: 'SeqClear' with no args")
			
		for n in tnames:
			tnames[n].clearSequence()
		self.futureVol = []
	
	def seqRnd(self, ln):
		""" Set random order for all tracks. """
		
		if ln:
			error("Use: SeqRnd with no paramater.")
			
		gbl.seqRnd = 1
		
	def seqNoRnd(self, ln):
		""" Reset random order for specified track. """
		
		if ln:
			error("Use: SeqNoRnd")
			
		gbl.seqRnd = 0
	
	
	def setLibPath(self, ln):
		""" Set the LibPath variable. This is a list. """

		gbl.libPath = list(ln)

			
	def setOutPath(self, ln):
		""" Set the Outpath variable. NOT a list. """
		
		if not ln:
			gbl.outPath = ""
			
		elif len(ln) > 1:
			error ("Use: SetOutPath PATH.")	
		
		else:
			gbl.outPath = ln[0]
		
	
	def beatAdjust(self, ln):
		""" Delete or insert some beats into the sequence."""
		
		if len(ln) != 1:
			error("Use: BeatAdjust NN")
		
		b = stof(ln[0], "Expecting a value (not %s) for BeatAdjust." % ln[0])
		gbl.tickOffset += int( b * gbl.BperQ)

		if optons.debug:
			print "BeatAdjust: inserted %s at bar %s" % (b, gbl.barNum)
		
	def fermata(self,ln):
		""" Apply a fermata timing to the specified beat. """
		
		if len(ln) != 3:
			error("Use: Fermata 'offset' 'duration' 'adjustment'")
			
		offset = stoi(ln[0], "Expecting a value (not %s) for Fermata." % ln[0]) 
		if offset < -gbl.QperBar or offset > gbl.QperBar:
			warning("Fermata: %s is a large beat offset.")
			
		dur = stoi(ln[1], "Expecting a value (not %s) for Fermata." % ln[1])
		if dur < 1:
			error("Fermata duration must be greater than 0.")
		if dur > gbl.QperBar:
			warning("Fermata: %s is a large duration.")
		
		adj = stoi(ln[2], "Expecting a value (not %s) for Fermata." % ln[2])
		adj = float(adj)
		if adj< 100:
			warning("Fermata: Adjustment less than 100 is shortening beat value.")
			
		if adj == 100:
			error("Fermata: using value of 100 makes no difference, must be an error.")
		
		ferm = int(gbl.tempo / (adj/100))
		print offset, ferm, dur
		mtrks[0].addTempo(gbl.tickOffset + (gbl.BperQ * offset), ferm)
		mtrks[0].addTempo(gbl.tickOffset + (gbl.BperQ * (offset + dur)),
			gbl.tempo)

		if options.debug:
			print "Fermata: Beat %s, Duration %s, Change %s, Bar %s" % \
				(offset, dur, adj, gbl.barNum)

		
	def lnprint(self, ln):
		print self.curline.split(' ', 1)[1]
		
	def comment(self, ln):
		pass
		
		
	def trackDefPattern(self, name, ln):
		""" Define a pattern for a track. 
		
			Here we gather lines looking for a tailing '@', the
			gathered line is passed to the pattern class.
		"""
		
		if ln:
			pattern = ln.pop(0).upper()
		else:
			error("Define is expecting a pattern name.")
	
		if len(ln) == 3 and  ln[1] == '*':
			tnames[name].mulPattern(pattern, ln[0], ln[2])

		elif len(ln) == 3 and ln[1].upper() == 'SHIFT':
			tnames[name].shiftPattern(pattern, ln[0], ln[2])

		else:			
			ln = ' '.join(ln)
			while not ln.endswith('@'):
				l = self.infile.read()
				if not l:
					error("EOF encountered while defining pattern.")
					
				ln += l
	
			# Delete trailing '@' and ';'
			
			while ln.endswith('@') or ln.endswith(';') :
				ln=ln[:-1].strip()
			tnames[name].definePattern(pattern, ln)
	
	
	def trackCompress(self, name, ln):
		""" Set (unset) compress for track. """
		
		if not ln:
			error("Use: %s Compress <value>" % name)
			
		tnames[name].setCompress(ln)
		
	def trackChannel(self, name, ln):
		""" Set the midi channel for a track."""
		
		if not ln:
			error("Use: %s Channel" % name)
			
		tnames[name].setChannel(ln[0])
	
	def trackChShare(self, name, ln):
		""" Set MIDI channel sharing."""
		
		if len(ln) !=1:
			error("Use: %s ChShare TrackName" % name)
		
		tnames[name].setChShare(ln[0])
		
		
	def trackDirection(self, name, ln):
		""" Set scale/arp direction. """
		
		if not ln:
			error("Use: %s Direction OPT" % name)
			
		tnames[name].setDirection(ln)
		
	def trackScaletype(self, name, ln):
		""" Set the scale type. """
		
		if not ln:
			error("Use: %s ScaleType OPT" % name)
			
		tnames[name].setScaletype(ln)
				
	def trackSequence(self, name, ln):
		""" Define a sequence for a track. """
		
		if not ln:
			error ("Use: %s Sequence NAME [...]" % name)
									
		tnames[name].setSequence(ln)
	
	def trackInvert(self, name, ln):
		""" Set invert for track."""
		
		if not ln:
			error("Use: %s Invert N [...]." % name)
		
		tnames[name].setInvert(ln)
					
	def trackRtime(self, name, ln):
		""" Set random timing for specific track. """
		
		if not ln:
			error ("Use: %s RTime N [...]." % name)
		
		tnames[name].setRTime(ln)
	
	
	def trackRvolume(self, name, ln):
		""" Set random volume for specific track. """
		
		if not ln:
			error ("Use: %s RVolume N [...]." % name)
	
		tnames[name].setRVolume(ln)
		
	def trackRskip(self, name, ln):
		""" Set random skip for specific track. """
		
		if not ln:
			error ("Use: %s RSkip N [...]." % name)
	
		tnames[name].setRSkip(ln)

				
	def trackArtic(self, name, ln):
		""" Set articulation. """
		
		if not ln:
			error("Use: %s Articulation N [...]." % name)
				
		tnames[name].setArtic(ln)
	
	def trackCresc(self, name, ln):
		error("(De)Crescendo only supported in master context.")
		
				
	def trackVolume(self, name, ln):
		""" Set volume for specific track. """
	
		if not ln:
			error ("Use: %s Volume DYN [...]." % name)
			
		tnames[name].setVolume(ln)

	
	def trackChannelVol(self, name, ln):
		""" Set the channel volume for a track."""
		
		if len(ln) != 1:
			error("Use: %s ChannelVolume.")
		
		v=stoi(ln[0], "Expecting integer arg, not %s." % ln[0])	
			
		if v<0 or v>127:
			error("ChannelVolume must be 0..127")
			
		tnames[name].setChannelVolume(v)
								
				
	def trackOctave(self, name, ln):
		""" Set octave for specific track. """
		
		if not ln:
			error ("Use: %s Octave N [...], (n=0..10)" % name)
		
		tnames[name].setOctave( ln )
						
	def trackVoice(self, name, ln):
		""" Set voice for specific track. """
		
		if not ln:
			error ("Use: %s Voice NN [...]" % name)
	
		ch = tnames[name].channel
		
		tnames[name].setVoice(ln)


	def trackStrum(self, name, ln):
		""" Set all specified track strum. """
		
		if not ln:
			error ("Use: %s Strum NN" % name)
			
		tnames[name].setStrum( ln )

	
	def trackSeqClear(self, name,  ln):
		""" Clear sequence for specified tracks.
		
		Note: "Drum SeqClear" clears all Drum tracks,
			"Drum3 SeqClear" clears track Drum3.
		"""
		
		for n in tnames:
			if n.find(name) == 0:
				tnames[n].clearSequence()
		
		
	def trackPan(self, name, ln):
		""" Set the Midi Pan value for a track."""
		
		if len(ln) != 1:
			error("Use: %s PAN NN" % name)
			
		v = stoi(ln[0], "Expecting integer value 0..127") 
		
		if v<0 or v>127:
			error("PAN value must be 0..127")

		mtrks[tnames[name].channel].addPan(gbl.tickOffset, v)
		
	
	def trackSeqRnd(self, name, ln):
		""" Set random order for specified track. """
		
		if ln:
			error("Use: %s SeqRnd with no paramater." % name)
			
		tnames[name].setRnd()

	def trackSeqNoRnd(self, name, ln):
		""" Reset random order for specified track. """
		
		tnames[name].setNoRnd()


	def trackOff(self, name, ln):
		""" Turn a track off """
		
		if ln:
			error("Use: %s OFF with no paramater." % name)

		tnames[name].setOff()


	def trackOn(self, name, ln):
		""" Turn a track on """
		
		if ln:
			error("Use: %s ON with no paramater." % name)

		tnames[name].setOn()


	def trackTone(self, name, ln):
		""" Set the tone (note). Only valid in drum tracks."""
		
		if not ln:
			error("Use: %s Tone N [...]." % name)
		
			
		tnames[name].setTone(ln)	

	def trackGlis(self, name, ln):
		""" Enable/disable portamento. """
		
		if len(ln) != 1:
			error("Use: %s Portamento NN, off=0, 1..127==on." % name)
			
		tnames[name].setGlis( stoi(ln[0], "Expecting integer for Portamento"))


	def trackGroove(self, name, ln):
		""" Select a previously defined groove for a single track. """
		
		if len(ln) != 1:
			error("Use: %s Groove Name" % name)
	
		tnames[name].restoreGroove(ln[0].upper())
		tnames[name].setSeqSize(gbl.seqSize)
		
		if options.debug:
			print("%s Groove settings restored from '%s'." % (name, ln[0]))
		
	


	def mdata(self, ln):
		""" Process a line of music data. """
				
				
		# a data line can have an option bar number at the start
		# of the line. Makes debugging input easier. The next
		# block strips leading integers off the line.
		# Note that a line number on a line by itself it okay.

		if ln:
			try:
				int(ln[0])
				del ln[0]
			except:
				pass
					
			if not ln:
				return

		rptcount = 1
			
		# Grab optional repeat count at end of bar
			
		if len(ln)>=2 and ln[-2] == '*':
			rptcount = ln[-1]
			del ln[-1]
			del ln[-1]
		elif len(ln)>=1 and ln[-1][0]=='*':
			rptcount = ln[-1][1:]
			del ln[-1]
				
		rptcount = stoi(rptcount, "Expecting integer after '*'")
				
		while ln < gbl.QperBar:
			ln.extend('/')

		for i in range(len(ln)):
			if ln[i] == '/':
				if not self.lastChord:
					error("A chord has to be set before"
						" you can use a '/'")
				else:
					ln[i] = self.lastChord

			self.lastChord = ln[i]
		
				
		ctable=self.mbar(ln)
		for rpt in range(rptcount):
			if self.futureVol:
				gbl.volume = self.futureVol.pop(0)

			if gbl.seqRnd:
				gbl.seqCount = random.randrange(gbl.seqSize)

			for a in tnames:
				tnames[a].bar(ctable)
			
			gbl.barNum += 1
			gbl.beatNum += gbl.QperBar
			gbl.tickOffset += (gbl.QperBar * gbl.BperQ)
			
			gbl.seqCount = (gbl.seqCount+1) % gbl.seqSize

			if options.showrun:
				print "%3d: %s" % (gbl.barNum,
					self.curline )


	def mbar(self, ln):
		""" Process expanded line for a voice.
		
			ctable is a list of CTables, one for each beat division.
			Each entry has the offset (in midi ticks), chordname, etc.
		
			This table is created once for each bar.	
		
		"""
		
		i = 0
		ctable=[]
		for l in ln:
			tt=CTable()
			if l.find('z')>=0:
				""" Treat a 'z' in the chord specification. 
				
				A 'z' can be of the form CmzX or just z.
					z      - play ONLY drum
					z!	- no tracks
					CHORDz - illegal
					zX     - illegal
					CzX    - play chord C, don't play tracks X where
						D = DRUM
						C = CHORD
						A = ARPEGGIO
						W = WALK
						B = BASS
						S = SCALE
				"""
				
				c,r=l.split('z')	# chord name/track mute
				
				if r=='!':
					r='DCAWBS'	# mute all
					if c=='':
						c='C'	# dummy chord name
						
				if c=='' and r:
					error("To mute individual tracks you must" 
					" use a chord/z combination not '%s'." % l)
					
				if c and r=='':
					error("'z' mutes all tracks so you can't include"
						" the chord '%s'." % l)
					
				if r=='':
					r='CBAWS'
					c='C'
				for v in r:
					if v == 'C':
						tt.chordZ = 1
					elif v == 'B':
						tt.bassZ = 1
					elif v == 'A':
						tt.arpeggioZ = 1
					elif v == 'W':
						tt.walkZ = 1
					elif v == 'D':
						tt.drumZ = 1
					elif v == 'S':
						tt.scaleZ = 1
					else:
						error("Unknown voice '%s' for rest in '%s'."
							% (v,l))

			else:
				c=l
			
			tt.offset = i * gbl.BperQ
			tt.chordname = c
			tt.chordnotes, tt.scale, tt.type = getChord(c)
			ctable.append(tt)
			i += 1
	
		return ctable	
			


###########################
# Miscellaneous Functions #
###########################

def getOffset(ticks, ran=0):
	""" Calculate a midi offset into a song."""

	p = gbl.tickOffset + ticks

	if ran:
		p += random.randrange(-ran,ran+1) 
	
	return int(p)
	

###########################################

def stoi(s, errmsg):
	""" string to integer. """
	try:
		return int(s)
	
	except:
		error(errmsg)

def stof(s, errmsg):
	""" String to floating point. """
	
	try:
		return float(s)
	except:
		error(errmsg)

######################################
# Return a dict key for a value

def keyLookup(d, v):
	for a in d:
		if d[a] == v:
			return a
	return ''


###########################################
# Convert Note to Ticks

def getNoteLen(n, noteLenTable = {
	'0':	1,		# special 0==1 midi tick
	'1': gbl.BperQ * 4,	# whole note
	'2': gbl.BperQ * 2,	# 1/2
	'4': gbl.BperQ,	# 1/4
	'8': gbl.BperQ / 2,	# 1/8
	'16': gbl.BperQ / 4,	# 1/16
	'32': gbl.BperQ / 8,	# 1/32
	'64': gbl.BperQ / 16,	# 1/64
	'3': gbl.BperQ*2/3 }	# 1/8 triplet

	):	
	
	""" Convert a Note to a midi tick length. 
	
		Notes are 1==Whole, 4==Quarter, etc.
		Notes can be combined: 1+4 == 5 beats, 4+8 == dotted 1/4
	"""
	
	l = 0
	for a in str(n).split('+'):
		try:
			l += noteLenTable[a]
		except:
			error("Unknown note duration %s" % n )
			
	return l


def printList(l):
	""" Print each item in a list. Works for numeric and string."""
	
	for a in l:
		print a,
	print


########################################
########################################

# Start of Program

###########################
# Get our command line stuff

options=Options("0.1")


##########################################
# Each mma track has a tnames{} entry. 
# Create a class instance for each possible track. There are
# 10 tracks for each type. Names of the tracks are BASS, BASS1..BASS9, etc.

tnames={} 

for f, name in ((Bass,     'BASS'), 
		(Chord,    'CHORD'),
		(Arpeggio, 'ARPEGGIO'),
		(Scale,    'SCALE'),
		(Walk,     'WALK'),
		(Drum,     'DRUM') ):


	tnames[name] = f( name ) 
	for a in range(1,10):
		n = name + str(a)
		tnames[n] = f( n )

# Assign Midi channels for selected tracks.
# Note that __init__() has already assigned track 10 to drum.

for a, chan in (
	('BASS',     3 ),  ('BASS1',     4 ), ('BASS2',  5 ),
	('CHORD',    6 ),  ('CHORD1',    7 ), ('CHORD2', 8 ),
	('ARPEGGIO', 9 ),  ('ARPEGGIO1', 11 ),
	('SCALE',    12 ), ('SCALE1',    13 ),
	('WALK',     14 ), ('WALK1',     15 ), ('WALK2', 16 ) ): 
	
	tnames[a].setChannel(str(chan))


#######################################
# Set up initial meta track stuff. Track 0 == meta

m = mtrks[0]=Mtrk(0)

m.addTrkName(0,'MetaTrack')
m.addTempo(0,gbl.tempo) 
m.addTimeSig(0, 4, 2, 48, 8) 
m.addText(0, "Created by MMA") 

#####################################
# Read an RC file. All found files are processed.

rcread=0
for i in ('/etc/mmarc', '/usr/local/etc/mmarc',
		os.path.expanduser("~/.mmarc"),	'mmarc' ):

	if os.path.exists(i):
		if options.showrun:
			print "Reading RC file '%s'" % i
		processFile(i)
		rcread+=1

if not rcread:
	warning("No RC file was found or processed")
	
# Libary path is current dir and "~/data/mma" if not set in rc files

if not gbl.libPath:
	gbl.libPath = [ '.' , '~/data/mma' ]

# Outfile prefix is current dir if not set in rc files

if not gbl.outPath:
	gbl.outPath = '.'


# Just display the channel assignments and exit.

if options.chshow:
	print "MMA channel assignments:"
	for c in range(1,17):
		k=tnames.keys()
		k.sort()
		for a in k:
			if tnames[a].channel == c:
				print " %2s  %s" % (c, tnames[a].name)
				break
	sys.exit(0)
	

################################
# Ready, set, go.

if not options.infile:
	options.usage("No input filename specified.")
	
mtrks[0].addTrkName(0, options.infile) 	# add infile name to meta track

if options.outfile:
	outfile = "%s/%s" % (gbl.outPath, options.outfile)
else:
	outfile = "%s/%s.mid" % (gbl.outPath, options.infile)


# Read/process the mmaStart, data and mmaEnd files.

if gbl.mmaStart:
	if not os.path.exists(gbl.mmaStart):
		warning("MmaStart file %s not processed." % gbl.mmaStart)
	else:
		processFile(gbl.mmaStart)

processFile(options.infile)

if gbl.mmaEnd:
	if not os.path.exists(gbl.mmaEnd):
		warning("MmaEnd file %s not processed." % gbl.mmaEnd)
	else:
		processFile(gbl.mmaEnd)

# Create the output file

gbl.lineno=-1	# disable line nums for error/warning

if options.noOutput:
	print "Input file parsed successfully. No midi file generated."
	sys.exit(0)

outfile=os.path.expanduser(outfile)

fileExist = os.path.exists(outfile)
if fileExist:
	warning( "Overwriting existing file '%s'" % outfile )
	
# Check all the tracks and find total number used.
# When initializing each track (class) we made an initial entry
# in the track at offset 0 for the track name, etc. So, if the
# track only has one entry we can safely skip the entire track.

keys=mtrks.keys()
keys.sort()
trackCount=1	# account for meta track

for n in keys[1:]:	 # check all but 0 (meta)
	if len(mtrks[n].miditrk) > 1:
		trackCount += 1

if trackCount == 1:	# only meta track
	if fileExist:
		print
	print "No data created. Did you remember to set a groove/sequence?"
	if fileExist:
		print "Existing file '%s' has not been modified." % outfile
	sys.exit(1)


print "Creating midi file '%s'" % outfile

try:
	out = open(outfile, 'w')
except:
	error("Can't open file '%s' for writing." % outfile)


# Write Midi file header.

out.write( "MThd" + intToLong(6) + intToWord(1) +
	intToWord(trackCount) + intToWord(gbl.BperQ) )

# Write meta track

mtrks[0].writeMidiTrack(out)

# Write the remaining tracks

for n in keys[1:]:
	if len(mtrks[n].miditrk) > 1:		
		mtrks[n].writeMidiTrack(out)	

out.close()

if options.debug:
	print "Closed file '%s'." % outfile



