gautrais hace 6 años
commit
87e2f6f057

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+__pycache/
+**/*.pyc
+

+ 39 - 0
apcmini.py

@@ -0,0 +1,39 @@
+#!/usr/bin/python
+
+from padadapter import PadAdapter
+from pad import LedButton, LedCtrlButton
+from simplemidi.options import *
+
+class APCMini(PadAdapter):
+	
+	_DEFAULT_PARAMS={
+		'name': 'Simple Midi AKAI APC Mini',
+		'autoconnect_in': 'APC MINI:APC MINI MIDI',
+		'autoconnect_out': 'APC MINI:APC MINI MIDI'
+	}
+	
+	def __init__(self, param={}):
+		PadAdapter.__init__(self, initParams(APCMini._DEFAULT_PARAMS, param))
+			
+			
+		
+	def onAdapt(self, pad):
+		pad.ledbuttonsWidth=8
+		pad.ledbuttonsHeight=8
+		
+		for i in range(pad.ledbuttonsHeight):
+			for j in range(pad.ledbuttonsWidth):
+				pad.ledbuttons[j+i*pad.ledbuttonsWidth]=LedButton(pad.oport, j+i*pad.ledbuttonsWidth)
+		
+		for i in range(64, 72):
+			pad.ledbuttons[i]=LedCtrlButton(pad.oport, i)
+			
+		for i in range(82, 90):
+			pad.ledbuttons[i]=LedCtrlButton(pad.oport, i)
+		pad.ledbuttons[98]=LedButton(pad.oport, i)	
+		
+		for i in pad.ledbuttons.keys():
+			pad.input[i]=0
+		
+		#pad.clear()
+		

+ 0 - 0
compose/__init__.py


+ 104 - 0
main.py

@@ -0,0 +1,104 @@
+#!/usr/bin/python
+
+from serpentgame import SerpentAPCMini, setRealtime
+from apcmini import APCMini
+from paintpad import PaintPad
+from simplemidi.midiio import MidiInputPort, MidiOutputPort
+from simplemidi.midiplayer import MidiPlayer
+from padhelper import PadHelper
+from syncpadhelper import SyncPadHelper
+from simplemidi.alsaconnector import AlsaConnector
+import time
+import sys
+import rtmidi
+from simplemidi.options import *
+from simplemidi.midieventtrigger import *
+
+#path="/home/fanch/Documents/tabs/Misc Children - Twinkle Twinkle Little Star2.mid"
+#path="/home/fanch/Documents/tabs/Johann Pachelbel - Canon In D (ver 6 by Ezechiel).mid"
+path="/home/fanch/Documents/tabs/Misc Traditional - Katyusha.mid"
+#path="/home/fanch/Documents/tabs/JerryC - Canon Rock.mid"
+"""
+if __name__=="__main__":	
+	#
+	m=MidiPlayer(path)
+	m.setBpmRatio(1)
+	m.transpose(-12)
+	input("Press Enter to continue...")
+	#m.connect((28,0))
+	m.play()
+"""
+def _test_midiPlayer():
+	m=MidiPlayer(path,{})
+	m.setBpmRatio(1)
+	m.transpose(48)
+	input("...")
+	m.play()
+
+
+def _test_syncPadHelper():
+	m = SyncPadHelper(APCMini(), path, {
+		'transpose': 24,
+		'pad_translate' : -24
+		
+	})
+	m.setBpmRatio(1)
+	m.setPadTrack(1)
+	input("...")
+	m.play()
+
+def _test_padHelper():
+	m = PadHelper(APCMini(), path, {})
+	m.setBpmRatio(1)
+	m.setPadTrack(1)
+	m.setPadTranslate(0)
+	m.transpose(0)
+	input("...")
+	m.play()
+
+def _test_paintPad():
+	apc=APCMini()
+
+	jeu=PaintPad(apc, {})
+	jeu.start()
+
+def _test_serpent():
+	apc=APCMini()
+	#setRealtime()
+	jeu = SerpentAPCMini(apc)
+	jeu.waitForInput()
+	jeu.loadingScreen(2)
+	jeu.start()
+	jeu.close()
+
+			
+def testCb(x, y):
+	print("Trigger : ", x)
+
+def testTrigger():
+	t=MidiMultiTrigger()
+	ip=MidiInputPort({'port_name': 'In'})
+	ip.open()
+	ip.connect((28,0))
+	
+	op=MidiInputPort({'port_name': 'Out'})
+	op.open()
+	op.connect((28,0))
+	t.addTrigger(1, (MidiEventTrigger.NOTE, (16,32), None, False))
+	
+	while True:
+		evt=ip.getSync()
+		print(t.filter(evt, op))
+		
+
+if __name__=="__main__":	
+	n=6
+	if len(sys.argv)>1: n=int(sys.argv[1])
+	
+	if n==0: testTrigger()
+	if n==1: _test_syncPadHelper()
+	if n==2: _test_padHelper()
+	if n==3: _test_midiPlayer()
+	if n==4: _test_paintPad()
+	if n==5: _test_serpent()
+	if n==5: _test_()

+ 13 - 0
miditranslate.py

@@ -0,0 +1,13 @@
+#!/usr/bin/python3
+#
+from simplemidi.midiio import MidiInputPort, MidiOutputPort
+from  padhelper import *
+from simplemidi.midieventtrigger import *
+from padhelper import PadHelper
+from apcmini import APCMini
+from padrouter import PadRouter
+
+ph = PadRouter(APCMini())
+
+ph.setPadTranslate(-12*6)
+while True: ph.waitForInput()

+ 577 - 0
pad.py

@@ -0,0 +1,577 @@
+#!/usr/bin/python
+# Type of LED Functions.
+# 0=off,
+# 1=green,
+# 2=green blink,
+# 3=red,
+# 4=red blink,
+# 5=yellow,
+# 6=yellow blink,
+# 7-127=green,
+
+# Type of LED Ring Functions
+# 0=off,
+# 1=Single,
+# 2=Volume Style,
+# 3=Pan Style,
+# 4-127=Single
+
+import logging
+import sys
+import time
+import rtmidi
+import random
+
+from rtmidi.midiutil import open_midioutput
+from rtmidi.midiconstants import NOTE_OFF, NOTE_ON
+from simplemidi.midiio import MidiInputPort, MidiOutputPort
+from simplemidi.midimessage import MidiType
+from simplemidi.alsaconnector import AlsaConnector
+from simplemidi.options import *
+"""
+Classe qui represente un bouton
+"""
+class Button:
+	def __init__(self, port, index, channel=1):
+		self.channel=channel # canal midi : int 
+		self.oport=port # port de sortie : MidiOut
+		self.index=index # index (=note) du bouton : int
+		Button.LED_UNKNOWN=-1 # etat du bouton  (couleur de la led) incoonu
+		Button.OFF=0
+		self.state=Button.LED_UNKNOWN # etat de la led : int
+		self.mindata=0 #valeur minimal que le bouton peut predre : int
+		self.maxdata=0 #valeur max que le bouton peut predre : int
+		self.values=[] # liste des valeurs que le bouton peut predre : int
+		self.lastState=0 # etat précédent (pour la fonction toggle()) : int
+	
+	def onNoteOn(self, channel, idx, val):
+		pass
+		
+		
+	def onNoteOff(self, channel, idx, val):
+		pass
+		
+	
+	"""
+	Envoi un note on pour changer de couleur
+	dat: int: couleur a mettre
+	"""
+	def send(self, data):
+		if data<=self.maxdata and data>=self.mindata:
+			self.state=data
+		if data!=Button.OFF and (data in self.values):
+			self.lastState=data
+		self.oport.noteOn(1, self.index, data)
+		time.sleep(0.00001)
+	
+	"""
+	Demande au bouton de passe à la couleur suivante
+	"""
+	def next(self):
+		if self.state==Button.LED_UNKNOWN:
+			return
+		x=self.state+1
+		if x>self.maxdata:
+			x=self.mindata
+		self.send(x)
+	
+	"""
+	Demande au bouton de passe à la couleur précédente
+	"""
+	def prev(self):
+		if self.state==Button.LED_UNKNOWN:
+			return
+		x=self.state-1
+		if x<self.mindata:
+			x=self.maxdata
+		self.send(x)
+	
+	"""
+	Prend (parmis les valeur valide) une couleur au hasard
+	"""
+	def random(self):
+		self.send(random.sample(self.values, 1)[0])
+	
+	"""
+	Bascule d'un état allumé à un etat eteint (et vis versa)
+	"""
+	def toggle(self):
+		if self.lastState==Button.OFF:
+			self.lastState=self.values[0]
+		if self.state!=Button.LED_UNKNOWN and self.state!=Button.OFF:
+			self.send(0)
+		else:
+			self.send(self.lastState)
+		
+"""
+Gere les boutons classiques a 7 états (les boutons de 0 a 63 dans 
+l' AKAI APC Mini
+"""
+class LedButton(Button):
+	def __init__(self, port, index, channel=1):
+		Button.__init__(self, port, index, channel)
+		LedButton.UNKNOWN=Button.LED_UNKNOWN
+		LedButton.OFF=0
+		LedButton.GREEN=1
+		LedButton.GREEN_BLINK=2
+		LedButton.RED=3
+		LedButton.RED_BLINK=4
+		LedButton.YELLOW=5
+		LedButton.YELLOW_BLINK=6
+		self.maxdata=6
+		self.values=[0,1,2,3,4,5,6]
+	
+	"""
+	Eteint la led du bouton
+	"""
+	def off(self):
+		self.send(LedButton.OFF)
+	
+	"""
+	Passe le bouton en vert
+	blink: bool : Fait clignter la led
+	"""
+	def green(self, blink=False):
+		self.send(LedButton.GREEN_BLINK if blink else LedButton.GREEN)
+	
+	"""
+	Passe le bouton en rouge
+	blink: bool : Fait clignter la led
+	"""
+	def red(self, blink=False):
+		self.send(LedButton.RED_BLINK if blink else LedButton.RED)
+	
+	"""
+	Passe le bouton en jaune
+	blink: bool : Fait clignter la led
+	"""
+	def yellow(self, blink=False):
+		self.send(LedButton.YELLOW_BLINK if blink else LedButton.YELLOW)
+
+	
+	
+	
+class LedCtrlButton(Button):
+	def __init__(self, port, index, channel=1):
+		Button.__init__(self, port, index, channel)
+		LedCtrlButton.UNKNOWN=Button.LED_UNKNOWN
+		LedCtrlButton.OFF=0
+		LedCtrlButton.ON=1
+		LedCtrlButton.BLINK=2
+		self.maxdata=2
+		self.values=[0,1,2]
+	
+	def off(self):
+		self.send(LedCtrlButton.OFF)
+	
+	def on(self):
+		self.send(LedCtrlButton.ON)
+	
+	def blink(self):
+		self.send(LedCtrlButton.BLINK)
+
+
+
+
+MIDI_ERROR_STRING={
+	rtmidi.ERRORTYPE_WARNING:"ERRORTYPE_WARNING",
+	rtmidi.ERRORTYPE_DEBUG_WARNING:"ERRORTYPE_DEBUG_WARNING",
+	rtmidi.ERRORTYPE_UNSPECIFIED:"ERRORTYPE_UNSPECIFIED",
+	rtmidi.ERRORTYPE_NO_DEVICES_FOUND:"ERRORTYPE_NO_DEVICES_FOUND",
+	rtmidi.ERRORTYPE_INVALID_DEVICE:"ERRORTYPE_INVALID_DEVICE",
+	rtmidi.ERRORTYPE_MEMORY_ERROR:"ERRORTYPE_MEMORY_ERROR",
+	rtmidi.ERRORTYPE_INVALID_PARAMETER:"ERRORTYPE_INVALID_PARAMETER",
+	rtmidi.ERRORTYPE_INVALID_USE:"ERRORTYPE_INVALID_USE",
+	rtmidi.ERRORTYPE_DRIVER_ERROR:"ERRORTYPE_DRIVER_ERROR",
+	rtmidi.ERRORTYPE_SYSTEM_ERROR:"ERRORTYPE_SYSTEM_ERROR",
+	rtmidi.ERRORTYPE_THREAD_ERROR:"ERRORTYPE_THREAD_ERROR"
+}
+
+
+"""
+Classe qui gère un Pad
+"""
+
+class Pad():
+	_DEFAULT_PARAMS={
+		'port_in': MidiInputPort._DEFAULT_PARAMS,
+		'port_out': MidiOutputPort._DEFAULT_PARAMS
+	}
+	
+	def __init__(self, adapter, params={}):
+		param=initParams(Pad._DEFAULT_PARAMS, params)
+		
+		self.oport=MidiOutputPort.fromParams(param['port_out']) 
+		self.iport=MidiInputPort.fromParams(param['port_in']) 
+		
+		self.iport.setInputCallback(self.inputMessage, self)
+		self.iport.setErrorCallback(self.inputError, self)
+		
+		self.ledbuttons={} # liste des boutons
+		self.input={} # listes des états des boutons
+		
+		self.ledbuttonsWidth=0  # nombre de bouton de note par ligne
+		self.ledbuttonsHeight=0 # nombre de bouton de note par collone
+		self.adapt(adapter)
+		self.clear()
+	
+	"""
+	Permet d'appeler un adapteur qui permettra de configurer les boutons
+	"""
+	def adapt(self, adapter):
+		adapter.adapt(self)
+		self.onAdapt()
+	
+	
+	
+	"""
+	Appelé quand le pad a ete adapté
+	"""
+	def onAdapt(self):
+		pass
+	
+	"""
+	Attend tant qu'un boutton est presse ou relache
+	idx: int : L'id de la touche, un liste d'id ou None pour n'importe que touche
+	state: bool: True pour une attente de NOTE_ON False pour NOTE_OFF
+	t: float : temps a attendre entre 2 poll
+	func: callback : permet d appeler une callback a chaque sondage
+	data: * : permet de passer un parametre a la fonction
+	Retourne l'id qui donne le signal
+	"""
+	def waitForInput(self, idx=None, state=True, t=0.01, func=None, data=None):
+		while True:
+			i=self.pollInput(idx, state)
+			
+			if i!=None:
+				return i
+			
+			if func!=None:
+				func(data)		
+			time.sleep(t)
+			
+			
+	"""
+	Sonde si un boutton est presse ou relache
+	idx: int : L'id de la touche, un liste d'id ou None pour n'importe que touche
+	state: bool: True pour une attente de NOTE_ON False pour NOTE_OFFnction
+	Retourne l'id qui du bouton ou None
+	"""
+	def pollInput(self, idx=None, state=True):
+		if isinstance(idx, int):
+			idx=[idx]
+		if idx==None:
+			idx=[]
+			for i in self.keys():
+				idx.append(i)
+				
+		for i in idx:
+			if self.input[i]==state:
+				return i
+		return None
+	
+	"""
+	Eteint tous les boutons
+	"""
+	def clear(self):
+		for k in self.keys():
+			self.ledbuttons[k].off()
+	
+	
+	"""
+	Fonction qui va recevoir toutes les erreurs MIDI du pad
+	"""
+	def inputError(self, code, msg):
+		ERR("Midi error:"+ str(MIDI_ERROR_STRING[code])+" : "+str(msg))
+	
+	"""
+	Fonction qui va recevoir tous les messages MIDI du pad
+	"""
+	def inputMessage(self, delta, msg):
+		channel=msg.channel
+		if msg.getType()==NOTE_ON:
+			index=msg.key
+			self._onNoteOn(channel, index,  msg.velocity)
+			if self.ledbuttons[index]:
+				self.ledbuttons[index].onNoteOn(channel, index, msg.velocity)
+		elif msg.getType()==NOTE_OFF:
+			index=msg.key
+			self._onNoteOff(channel, index,  msg.velocity)
+			if self.ledbuttons[index]:
+				self.ledbuttons[index].onNoteOff(channel, index, msg.velocity)
+		elif msg.getType()==MidiType.CONTROL_CHANGE:
+			index=msg.key
+			self.onControlChange(channel,index, msg.value)
+
+		
+		
+	"""
+	Fonction appellé en cas d'appui sur une touche
+	"""	
+	def onNoteOn(self, channel, idx, val):
+		pass
+		
+		
+	"""
+	Fonction appellé quand une touche est relachée
+	"""	
+	def onNoteOff(self, channel, idx, val):
+		pass
+		
+		
+	
+	def onControlChange(self, channel, key, value):
+		pass
+	
+	
+	def _onNoteOn(self, channel, idx, val):
+		self.input[idx]=True
+		self.onNoteOn(channel, idx, val)
+		
+		
+	def _onNoteOff(self, channel, idx, val):
+		self.input[idx]=False
+		self.onNoteOff(channel, idx, val)
+	
+	def getInputState(self, idx):
+		if idx in self.input.keys():
+			return self.input[idx]
+		return None
+	
+	@staticmethod
+	def matrixToArray(mat):
+		out=[]
+		for y in mat:
+			for x in y:
+				out.append(x)
+		return out
+		
+	"""
+	Mappe un vecteur ou une matrice sur une partie du pad
+	"""	
+	def mapSubRect(self, data, start, end=None):
+		if (isinstance(data, list) or isinstance(data, tuple)) and  len(data)>0:
+			if (isinstance(data[0], list) or isinstance(data[0], tuple)):
+				end=(start[0]+len(data[0]), start[1]+len(data[1]))
+				data=Pad.matrixToArray(data)
+			if end==None:
+				end=(self.ledbuttonsWidth, self.ledbuttonsHeight)
+			w=end[0]-start[0]
+			realw= w if start[0]+w<self.ledbuttonsWidth else self.ledbuttonsWidth
+			for j in range(start[1], end[1]):
+				for i in range(start[0], end[0]):
+					if i>= realw: continue
+					n=(j-start[1])*w+(i-start[0])
+					self[i,j]=data[n]
+
+	def fill(self, value, matrixOnly=True):
+		if matrixOnly:
+			x=[]
+			for i in range(self.ledbuttonsWidth*self.ledbuttonsHeight):
+				x.append(value)
+			self.map(x)
+		else:
+			for k in self.keys():
+				self[k]=value
+	
+	def blink(self, value, n=2):
+		self.fill(value)
+		time.sleep(n/2.0)
+		
+	def loadingScreen(self, tt):
+		t=tt/9.0
+		for i in range(64,64+8):
+			self[i].on()
+			time.sleep(t)
+			self[i].off()
+		for k in range(2):
+			for i in range(64,64+8):
+				self[i].on()
+			time.sleep(t/3)
+			for i in range(64,64+8):
+				self[i].off()
+			if k<2: time.sleep(t/3)
+			
+	
+	def showText(self, text):
+		mat=text.getDataToShow()
+		self.mapSubRect(mat, text.pos, (text.dim[0]+text.pos[0],text.dim[1]+text.pos[1]))
+		text.step()
+		
+	"""
+	Mappe un vecteur ou une matrice tous les boutons de notes
+	"""	
+	def map(self, data):
+		first=self.ledbuttons[0].index
+		self.mapSubRect(data, (int(first/self.ledbuttonsWidth),first%self.ledbuttonsWidth))
+	
+	#liste des id des boutons
+	def keys(self):
+		return self.ledbuttons.keys()
+
+	# transforeme une coordonnée d'uune matrice ou d'un vecteur en une coordonée d'un vecteur
+	def getIndex(self, x, y=-1):
+		return x if y<0 else x+y*self.ledbuttonsWidth
+	
+	# renvoie le bouton a l'adresse x, y
+	def getButton(self, x, y=-1):
+		return self.ledbuttons[self.getIndex(x,y)]
+	
+	def __getitem__(self,key):
+		if isinstance(key, tuple):
+			return self.getButton(key[0], key[1])
+		else:
+			return self.getButton(key)
+	
+	def __setitem__(self,key,value):
+		if isinstance(key, tuple):
+			x=self.getButton(key[0], key[1])
+			if x:
+				x.send(value)
+		else:
+			x=self.getButton(key).send(value)
+			if x:
+				x.send(value)
+	
+	
+	def connectOut(self, port):
+		port=AlsaConnector.portAuto(port)
+		self.oport.connect(port)
+		INFO("Connected to input port: "+str(port)+"\n")
+	
+	
+	def connectIn(self, port):
+		port=AlsaConnector.portAuto(port)
+		self.iport.connect(port)
+		INFO("Connected to output port: "+str(port)+"\n")
+		
+		
+	
+	def close(self):
+		self.oport.close()
+		self.iport.close()
+
+TEXT_DATA={
+	'A': [[1,1,1,0], [1,0,1,0], [1,1,1,0], [1,0,1,0]],
+	'R': [[1,1,1,0], [1,1,0,0], [1,0,1,0], [1,0,1,0]],
+	'T': [[1,1,1,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]],
+	'Y': [[1,0,1,0], [1,0,1,0], [0,1,0,0], [0,1,0,0]],
+	'G': [[1,1,1,0], [1,0,0,0], [1,0,1,0], [1,1,1,0]],
+	'I': [[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]],
+	'L': [[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,1,0]],
+	'N': [[1,0,1,0], [1,1,1,0], [1,1,1,0], [1,0,1,0]],
+	'O': [[1,1,1,0], [1,0,1,0], [1,0,1,0], [1,1,1,0]],
+	'C': [[1,1,1,0], [1,0,0,0], [1,0,0,0], [1,1,1,0]],
+	'S': [[1,1,1,0], [1,1,1,0], [0,0,1,0], [1,1,1,0]],
+	' ': [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]],
+	':': [[0,0,0,0], [0,1,0,0], [0,0,0,0], [0,1,0,0]],
+	'0': [[1,1,1,0], [1,0,1,0], [1,0,1,0], [1,1,1,0]],
+	'1': [[0,1,0,0], [1,1,0,0], [0,1,0,0], [0,1,0,0]],
+	'2': [[1,1,1,0], [0,0,1,0], [0,1,0,0], [1,1,1,0]],
+	'3': [[0,1,1,0], [0,0,1,0], [0,1,1,0], [0,1,1,0]],
+	'4': [[0,0,1,0], [0,1,1,0], [1,1,1,1], [0,0,1,0]],
+	'5': [[1,1,1,0], [1,0,0,0], [1,1,1,0], [1,1,1,0]],
+	'6': [[0,1,1,0], [0,1,0,0], [0,1,1,0], [0,1,1,0]],
+	'7': [[1,1,0,0], [0,1,0,0], [1,1,1,0], [0,1,0,0]],
+	'8': [[1,1,1,0], [1,1,1,0], [1,0,1,0], [1,1,1,0]],
+	'9': [[0,1,1,0], [0,1,1,0], [0,0,1,0], [0,1,1,0]],
+	'M': [[1,0,1,0], [1,1,1,0], [1,1,1,0], [1,1,1,0]],
+	'E': [[0,1,1,0], [0,1,1,0], [0,1,0,0], [0,1,1,0]],
+	'V': [[1,0,1,0], [1,0,1,0], [1,0,1,0], [0,1,0,0]],
+	'!': [[0,1,0,0], [0,1,0,0], [0,0,0,0], [0,1,0,0]],
+	'?': [[1,1,1,0], [0,1,1,0], [0,0,0,0], [0,1,0,0]]
+}
+
+"""
+Classe qui gère les donnes a afficher pour afficher du texte défilant
+"""
+class Text:
+	def __init__(self, txt, dim, pos=(0,0)):
+		self.pos=pos # position : (x, y)
+		self.dim=dim # dimension : (w,h)
+		self.txt=txt.upper() # texte a afficher : str
+		self.len=len(txt)*4 # longueur en cases (pas en caracteres) : int
+		self.matrix=[] # toutes les données : Matrix [ [..] .. [..] ]
+		for y in range(4):
+			for x in range(self.len):
+				self.matrix.append(0)
+				
+				
+		self.offset=-1 # décalage de la partie a afficher actuellement : in
+		self.color=1 # couleur du text : int
+		self.backgroundcolor=0 #couleur du fond : int
+		for i in range(len(self.txt)):
+			self.map(self.txt[i], i*4)
+			
+	"""
+	Récupère un vecteur de bit a afficher pour une lettre
+	color: int : Couleur du texte
+	backcolor: int : couleur du fond
+	"""
+	@staticmethod
+	def letterVector(char, color=1, backcolor=0):
+		x=[]
+		c=TEXT_DATA[char]
+		for j in reversed(range(4)):
+			for i in range(4):
+				x.append(color if c[j][i]>0 else backcolor)
+		return x
+	
+	"""
+	Map un vecteur ou une matrix dans les données a afficher
+	"""
+	def map(self, char, off):
+		c=TEXT_DATA[char]
+		for j in reversed(range(4)):
+			for i in range(off, off+4):
+				self.matrix[i+(3-j)*self.len]=c[j][i-off]
+	
+	"""
+	Change la couleur du text
+	c: int : Couleur du texte
+	"""
+	def setColor(self, c):
+		self.color=c
+		
+	"""
+	Change la couleur du fond
+	c: int : Couleur du fond
+	"""
+	def setBackgroundColor(self, c):
+		self.backgroundcolor=c
+	
+	"""
+	Recupere le vecteur a mapper sur le pad
+	"""
+	def getDataToShow(self):
+		out=[]
+		for j in range(4):
+			for i in range(self.offset, self.offset+self.dim[0]):
+				i=i%self.len
+				xx=self.matrix[i+j*self.len]
+				x= self.color if xx>0 else self.backgroundcolor
+				out.append(x)
+		return out
+	
+	"""
+	Donne le nombre d'étape pour afficher completement le text n fois
+	n: (int = 1) : Nombre de fois a afficher
+	"""
+	def stepCount(self, n=1):
+		return (self.len-self.dim[0]+1)*n
+	
+	"""
+	Décale d'une position (case) le texte
+	"""
+	def step(self):
+		self.offset=(self.offset+1)%self.len
+
+import os
+def setRealtime(rt=os.SCHED_FIFO):
+	try:
+		os.sched_setscheduler(0, rt, os.sched_param(0))
+		return True
+	except Exception as err:
+		ERR("Impossible de passer en temps-réel: "+str(err))
+		return False
+		

+ 64 - 0
padadapter.py

@@ -0,0 +1,64 @@
+#!/usr/bin/python
+
+from simplemidi.alsaconnector import AlsaPort
+from simplemidi.options import *
+
+class PadAdapter:
+	_DEFAULT_PARAMS={
+		'name': None,
+		'autoconnect_in': None,
+		'autoconnect_out': None
+	}
+	
+	def __init__(self, param):
+		param=initParams(PadAdapter._DEFAULT_PARAMS, param)
+		self.name=param['name']
+		self.autoIn=param['autoconnect_in']
+		self.autoOut=param['autoconnect_out']
+	
+	def adapt(self, pad):
+		self.__init_port_out(pad)
+		self.__init_port_in(pad)
+		self.onAdapt(pad)
+		
+	
+	
+	def __init_port_in(self, pad):
+		if not pad.iport.isOpen():
+			self.initPortIn(pad)
+		if self.autoIn: 
+			self.autoconnectIn(pad)
+			
+	def __init_port_out(self, pad):
+		if not pad.oport.isOpen():
+			self.initPortOut(pad)
+		if self.autoOut: 
+			self.autoconnectOut(pad)
+		
+		
+	def initPortIn(self, pad):
+		pad.iport.open()
+		
+	def initPortOut(self, pad):
+		pad.oport.open()
+		
+	def autoconnectIn(self, pad):
+		i=0
+		x= pad.iport.getPorts()
+		for p in x:
+			if str(p).find(self.autoIn)>=0:
+				a=AlsaPort(p)
+				pad.connectIn(a.globalId)
+			i=i+1
+		
+	def autoconnectOut(self, pad):
+		i=0
+		x= pad.oport.getPorts()
+		
+		for p in x:
+			if str(p).find(self.autoOut)>=0:
+				a=AlsaPort(p)
+				pad.connectOut(AlsaPort(p).globalId)
+			i=i+1
+
+

+ 154 - 0
padhelper.py

@@ -0,0 +1,154 @@
+#!/usr/bin/python
+
+
+from simplemidi.midiplayer import MidiPlayer, _Event, sortTime
+from simplemidi.midiio import MidiOutputPort
+from simplemidi.midimessage import MidiType, MidiMessage, NoteOn, NoteOff, MidiVoiceMessage
+from pad import Pad
+from simplemidi.options import *
+import time
+from simplemidi.midieventtrigger import *
+
+class PadHelper(MidiPlayer, Pad):
+	
+	_DEFAULT_PARAMS=dictAssign(MidiPlayer._DEFAULT_PARAMS,
+							   Pad._DEFAULT_PARAMS,{	   
+		'port_in': {
+			'client_name' : 'Pad Helper',
+			'port_name' : 'Pad In'
+		},   
+		'port_out': {
+			'client_name' : 'Pad Helper',
+			'port_name' : 'Pad Out'
+		},  
+		'port_sound_out': {
+			'client_name' : 'MidiPlayer'
+		},
+		'pad_track': -1,
+		'pad_translate' : 0,
+		'n_color' : 1
+		
+	})
+	
+	
+	
+	def __init__(self, adapter, path, params):
+		param=initParams(PadHelper._DEFAULT_PARAMS, params)
+		Pad.__init__(self, adapter, param)
+		MidiPlayer.__init__(self, path, param)
+		
+		self.padTrack = param['pad_track']
+		self.padTranslate=param['pad_translate']
+		self.nColor=param['n_color']
+		
+		if self.padTrack==-1:
+			if self.nTotalTracks>1: self.padTrack=1
+			else: self.padTrack=0
+			
+	
+	def setPadTrack(self, n):
+		self.padTrack=n
+		self._load_track_event()
+	
+	def setNKColor(self, n):
+		self.nColor=n
+		self._load_track_event()
+	
+	def setPadTranslate(self, n):
+		self.padTranslate=n
+	
+	
+	
+	
+	def onNoteOn(self, ch, idx, val):
+		self.port.noteOn(ch, idx-self.padTranslate, val)
+		
+		
+	def onNoteOff(self, ch, idx, val):
+		self.port.noteOff(ch, idx-self.padTranslate, val)
+	
+	
+	def send(self, evt) :
+		if evt.track==self.padTrack :
+			if evt.type==MidiType.NOTE_ON:
+				if (evt.key+self.padTranslate+self.transposeNote) in self.ledbuttons: self[evt.key+self.padTranslate+self.transposeNote]=evt.evt.velocity
+		else:
+			MidiPlayer.send(self, evt)
+	
+	def __note_to_notify(self, track, evt, delta, value):
+		ch=evt.channel
+		key=evt.key
+		return _Event(track, ( evt.tick-delta, NoteOn.new(1, key, value)))
+		#return _Event(track, ( evt.tick-delta, NoteOn.new(ch, key, value)))
+	
+	def __notify_off(self, track, evt):
+		return self.__note_to_notify(track, evt, 0, 0)
+		
+	def __notify_on(self, track, evt):
+		return self.__note_to_notify(track, evt, 0, 1)
+	
+	def __notify_croche(self, track, evt):
+		return self.__note_to_notify(track, evt, self.tickCroche(), 5)
+		
+	def __notify_noire(self, track, evt):
+		return self.__note_to_notify(track, evt, self.tickNoire(), 3)
+		
+	def __notify_blanche(self, track, evt):
+		return self.__note_to_notify(track, evt, self.tickBlanche(), 4)
+	
+	def _load_track_event(self):
+		self.events=[]
+		track=self.padTrack
+		for evt in self.tracks[track]:
+			e=_Event(track, evt)
+			if e.type==MidiType.NOTE_ON:
+				if self.nColor>=0: 
+					self.events.append(self.__notify_on(track,e))
+					
+				if self.nColor>=1: 
+					self.events.append(self.__notify_croche(track,e))
+					
+				if self.nColor>=2: 
+					self.events.append(self.__notify_noire(track,e))
+					
+				if self.nColor>=3:
+					self.events.append(self.__notify_blanche(track,e))
+					
+			if e.type==MidiType.NOTE_OFF:
+				self.events.append(self.__notify_off(track,e))	
+				
+		for track in range(len(self.tracks)):
+			if track==self.padTrack: continue
+			for evt in self.tracks[track]:
+				self.events.append(_Event(track, evt))	
+				
+		self.events.sort(key=sortTime)
+	
+	def onControlChange(self, ch, key, value):
+		if ( ch==1 and key==56):
+			rmin=0.25
+			rmax=4
+			if value==0: ratio=rmin
+			else: ratio=(value/127.0)*(rmax-rmin)+rmin
+			self.setBpmRatio(ratio)
+			DEBUG("BPM ratio: %.0f" % ratio, " BPM: %.0f" % (self.bpm*ratio))
+			
+		
+	
+	def play(self):
+		self._load_track_event()
+		self.tick=0
+		self.startTime=time.time()
+		for evt in self.events:
+			evt=evt.transpose(self.transposeNote)
+			self.waitForEvent(evt)
+			
+			
+			#if evt.track==self.padTrack and evt.type==MidiType.NOTE_ON:
+			#	self[evt.key+self.padTranslate]=evt.evt.velocity
+				
+			self.send(evt)
+			
+		
+		
+		

+ 36 - 0
padlghter.py

@@ -0,0 +1,36 @@
+
+from simplemidi.midiplayer import MidiPlayer, _Event, sortTime
+from simplemidi.midiio import MidiOutputPort
+from simplemidi.midimessage import MidiType, MidiMessage, NoteOn, NoteOff, MidiVoiceMessage
+from pad import Pad
+from simplemidi.options import *
+import time
+from simplemidi.midieventtrigger import *
+
+
+class PadLighter(Pad):
+    _DEFAULT_PARAMS = dictAssign(Pad._DEFAULT_PARAMS, {
+                                     'port_in': {
+                                         'client_name': 'Pad Helper',
+                                         'port_name': 'Pad In'
+                                     },
+                                     'port_out': {
+                                         'client_name': 'Pad Helper',
+                                         'port_name': 'Pad Out'
+                                     },
+                                     'port_sound_out': {
+                                         'client_name': 'MidiPlayer'
+                                     },
+                                     'pad_track': -1,
+                                     'pad_translate': 0,
+                                     'n_color': 1
+
+                                 })
+
+    def __init__(self, adapter, params):
+        param=initParams(PadLighter._DEFAULT_PARAMS, params)
+        Pad.__init__(self, adapter, param)
+        self.sound=MidiOutputPort.fromParams(param['port_sound_out'])
+        self.sound.open()
+
+

+ 52 - 0
padrouter.py

@@ -0,0 +1,52 @@
+
+from simplemidi.midiplayer import MidiPlayer, _Event, sortTime
+from simplemidi.midiio import MidiOutputPort
+from simplemidi.midimessage import MidiType, MidiMessage, NoteOn, NoteOff, MidiVoiceMessage
+from pad import Pad
+from simplemidi.options import *
+import time
+from simplemidi.midieventtrigger import *
+
+class PadRouter(Pad):
+    _DEFAULT_PARAMS = dictAssign(MidiPlayer._DEFAULT_PARAMS,
+         Pad._DEFAULT_PARAMS, {
+             'port_in': {
+                 'client_name': 'Pad Helper',
+                 'port_name': 'Pad In'
+             },
+             'port_out': {
+                 'client_name': 'Pad Helper',
+                 'port_name': 'Pad Out'
+             },
+             'port_sound_out': {
+                 'client_name': 'MidiPlayer'
+             },
+             'pad_translate': 0,
+
+         })
+
+
+    def __init__(self, adapter, params={}):
+        param = initParams(PadRouter._DEFAULT_PARAMS, params)
+        Pad.__init__(self, adapter, param)
+        self.padTranslate = param['pad_translate']
+
+
+
+
+    def setPadTranslate(self, n):
+        self.padTranslate = n
+
+    def onNoteOn(self, ch, idx, val):
+        self.oport.noteOn(ch, idx - self.padTranslate, val)
+
+    def onNoteOff(self, ch, idx, val):
+        self.oport.noteOff(ch, idx - self.padTranslate, val)
+
+    def send(self, evt):
+        if evt.track == self.padTrack:
+            if evt.type == MidiType.NOTE_ON:
+                if (evt.key + self.padTranslate + self.transposeNote) in self.ledbuttons: self[
+                    evt.key + self.padTranslate + self.transposeNote] = evt.evt.velocity
+        else:
+            MidiPlayer.send(self, evt)

+ 43 - 0
paintpad.py

@@ -0,0 +1,43 @@
+#!/usr/bin/python
+
+import time
+from pad import Pad
+from simplemidi.midimessage import MidiType
+from simplemidi.options import *
+
+class PaintPad(Pad):
+	_DEFAULT_PARAMS=dictAssign(Pad._DEFAULT_PARAMS, {
+	})
+	def __init__(self, adapter, params):
+		param=initParams(PaintPad._DEFAULT_PARAMS, params)
+		Pad.__init__(self, adapter, {})
+		self.learn=True
+		self.iport.setInputCallback(self.onControlChange, self, MidiType.CONTROL_CHANGE)
+	
+		
+	
+	def onNoteOn(self, ch, idx, val):
+		if idx==89:
+			self.clear()
+			
+		elif idx==84:
+			if self.learn:
+				self[idx].off()
+			else:
+				self[idx].on()
+			self.learn= not self.learn()
+		elif idx==98:
+			pass
+		elif self.input[98]:
+			self[idx].prev()
+		else:
+			self[idx].next()
+	
+	def clear(self):
+		
+		Pad.clear(self)
+	
+	def start(self):
+		self.clear()
+		while True:
+			time.sleep(1)

+ 67 - 0
serpentdata.py

@@ -0,0 +1,67 @@
+SERPENT_LEVELS=[
+	{ #0
+		'obstacles': [
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0]
+		],
+		'speeds': [1,0.9,0.81,0.73, 0.65, 0.59, 0.53, 0.47, 0.43, 0.38,0.34,0.3]
+	},
+	{ #1
+		'obstacles': [
+			[ 1,1,1,1,1,1,1,1],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 0,0,0,0,0,0,0,0],
+			[ 1,1,1,1,1,1,1,1]
+		],
+		'speeds': [0.81,0.73, 0.65, 0.59, 0.53, 0.47, 0.43, 0.38,0.34,0.3]
+	},
+	{ #2
+		'obstacles': [
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1]
+		],
+		'speeds': [0.81,0.73, 0.65, 0.59, 0.53, 0.47, 0.43, 0.38,0.34,0.3]
+	},
+	{ #3
+		'obstacles': [
+			[ 1,1,1,1,1,1,1,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,1,0,0,0,1],
+			[ 1,0,0,1,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,1,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,1,1,1,1,1,1,1]
+		],
+		'speeds': [0.73, 0.65, 0.59, 0.53, 0.47, 0.43, 0.38,0.34,0.3]
+	},
+	{ #4
+		'obstacles': [
+			[ 1,1,1,1,1,1,1,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,1,0,0,0,1],
+			[ 1,0,1,1,1,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,0,0,0,0,1],
+			[ 1,0,0,1,0,0,0,1],
+			[ 1,1,1,1,1,1,1,1]
+		],
+		'speeds': [0.73, 0.59, 0.53, 0.47, 0.43, 0.38]
+	}
+]

+ 353 - 0
serpentgame.py

@@ -0,0 +1,353 @@
+#!/usr/bin/python
+
+import time
+import random
+
+from pad import *
+from serpentdata import *
+from apcmini import APCMini
+
+"""
+Un element du serpent
+contient la position (x et y) et la direction
+"""
+class SerpentCase:
+	def __init__(self, x, y, dir):
+		self.x=x
+		self.y=y
+		self.dir=dir
+	
+	# retourne la position dans un tuple
+	def pos(self):
+		return (self.x, self.y)
+	
+	#retourne la prochaine position (au pas suivant) sans modifier l'objet
+	def peekNext(self):
+		if self.dir==SerpentAPCMini.UP:
+			return (self.x,(self.y+1)%8)
+		elif self.dir==SerpentAPCMini.DOWN:
+			return (self.x,(self.y-1)%8)
+		elif self.dir==SerpentAPCMini.LEFT:
+			return ((self.x-1)%8, self.y)
+		elif self.dir==SerpentAPCMini.RIGHT:
+			return ((self.x+1)%8, self.y)
+
+
+
+def obstacleFromMatrix(mat):
+	out=[]
+	for j in range(len(mat)):
+		for i in range(len(mat[j])):
+			if mat[j][i]:
+				out.append((i,len(mat)-j-1))
+	return out
+		
+	
+"""
+Classe qui gère le jeu du serpent sur l'AKAI APC Mini
+"""
+class SerpentAPCMini(Pad):
+	def __init__(self, adapter):
+		Pad.__init__(self, adapter)
+		SerpentAPCMini.UP=64
+		SerpentAPCMini.DOWN=65
+		SerpentAPCMini.LEFT=66
+		SerpentAPCMini.RIGHT=67
+		SerpentAPCMini.EXIT=98
+		SerpentAPCMini.EMPTY=0
+		SerpentAPCMini.SERPENT=1
+		SerpentAPCMini.FRUIT=2
+		SerpentAPCMini.OBSTACLE=2
+		SerpentAPCMini.SERPENT_COLOR=1
+		SerpentAPCMini.FRUIT_COLOR=5
+		SerpentAPCMini.OBSTACLE_COLOR=3
+		
+		self.lastTime=time.time()
+		self.dir=SerpentAPCMini.LEFT
+		self.changed=False
+		self.timeToWait=1
+		self.scoreleds=[89,88,87,86,85,84,83,82,71,70]
+		self.levels=SERPENT_LEVELS
+		self.levelIndex=0
+		#self.scoreleds=[70,71,82,83,84,85,86,87,88,89]
+		
+	"""
+	Gere les input, notamment l'allumage des leds des boutons directionnels
+	"""
+	def onNoteOn(self, channel, idx, val):
+		if idx==SerpentAPCMini.UP and self.serpent[0].dir!=SerpentAPCMini.DOWN:
+			self.changed=True
+			self.serpent[0].dir=idx;
+			self[idx].on()
+		elif idx==SerpentAPCMini.DOWN and self.serpent[0].dir!=SerpentAPCMini.UP:
+			self.changed=True
+			self.serpent[0].dir=idx;
+			self[idx].on()
+		elif idx==SerpentAPCMini.LEFT and self.serpent[0].dir!=SerpentAPCMini.RIGHT:
+			self.changed=True
+			self.serpent[0].dir=idx;
+			self[idx].on()
+		elif idx==SerpentAPCMini.RIGHT and self.serpent[0].dir!=SerpentAPCMini.LEFT:
+			self.changed=True
+			self.serpent[0].dir=idx;
+			self[idx].on()
+		elif idx==SerpentAPCMini.EXIT:
+			self.stop=True
+	
+	"""
+	Gere les input, notamment l'extinction des leds des boutons directionnels
+	"""
+	def onNoteOff(self, channel, idx, val):
+		if idx==SerpentAPCMini.UP and self.serpent[0].dir!=SerpentAPCMini.DOWN:
+			self[idx].off()
+		elif idx==SerpentAPCMini.DOWN and self.serpent[0].dir!=SerpentAPCMini.UP:
+			self[idx].off()
+		elif idx==SerpentAPCMini.LEFT and self.serpent[0].dir!=SerpentAPCMini.RIGHT:
+			self[idx].off()
+		elif idx==SerpentAPCMini.RIGHT and self.serpent[0].dir!=SerpentAPCMini.LEFT:
+			self[idx].off()
+	
+	"""
+	Retourne l'état d'une case d'un jeu (Serpent, fruit, obstacle ou vide)
+	"""
+	def stateAt(self, x, y=None):
+		if y==None and (isinstance(x,tuple) or isinstance(x,tuple)):
+			y=x[1]
+			x=x[0]
+		
+		for s in self.serpent:
+			if s.x==x and s.y==y:
+				return SerpentAPCMini.SERPENT
+		
+		for s in self.fruits:
+			if s[0]==x and s[1]==y:
+				return SerpentAPCMini.FRUIT
+				
+		for s in self.obstacle:
+			if s[0]==x and s[1]==y:
+				return SerpentAPCMini.FRUIT
+		
+		return SerpentAPCMini.EMPTY
+	
+	
+	"""
+	Renvoie une position disponible pour un nouveau fruit
+	"""
+	def nextFruit(self):
+		for i in range(64):
+			x=random.randint(0,7)
+			y=random.randint(0,7)
+			if self.stateAt(x,y)==SerpentAPCMini.EMPTY:
+				return (x,y)
+	
+	"""
+	Fonction d'affichage de la grille
+	"""
+	def refresh(self):
+		x=[]
+		for i in range(8):
+			x.append([0,0,0,0,0,0,0,0])
+		
+		for s in self.fruits:
+			x[s[1]][s[0]]=SerpentAPCMini.FRUIT_COLOR
+			
+		for s in self.serpent:
+			x[s.y][s.x]=SerpentAPCMini.SERPENT_COLOR
+				
+		for s in self.obstacle:
+			x[s[1]][s[0]]=SerpentAPCMini.OBSTACLE_COLOR
+		
+		for j in range(8):
+			for i in range(8):
+				self[i,j].send(x[j][i])
+	
+	"""
+	Vérifie si une collision a lieu
+	Retour:
+		True -> Collision
+		False -> Pas de collision
+	"""
+	def collision(self):
+		s=self.serpent[0].pos()
+		for i in range(1, len(self.serpent)):
+			ss=self.serpent[i].pos()
+			if ss[0]==s[0] and ss[1]==s[1]:
+				return True
+		for o in self.obstacle:
+			if o[0]==s[0] and o[1]==s[1]:
+				return True
+		return False
+	
+	"""
+	Affiche les score sur les leds ronde de droite
+	"""
+	def printScore(self): #binary
+		for i in range(len(self.scoreleds)):
+			if (self.score & (1<<i))>0:
+				self[self.scoreleds[i]].on()
+			else:
+				self[self.scoreleds[i]].off()
+			
+		
+	"""
+	Fait un pas dans le jeu (un mouvement automatique du serpent)
+	"""
+	def step(self):
+		s=(self.serpent[0].x, self.serpent[0].y)
+		
+		#modification de la vitesse en fonction du score
+		self.timeToWait=self.levels[self.levelIndex]['speeds'][self.score]
+		
+		#vérifie si le serpent mange un fruit
+		for x in self.fruits:
+			if s[0]==x[0] and s[1]==x[1]:
+				l=self.serpent[0]
+				self.serpent.append(SerpentCase(l.x, l.y, l.dir))
+				for fi in range(len(self.fruits)):
+					f=self.fruits[fi]
+					if f[0]==l.x and f[1]==l.y:
+						self.fruits[fi]=self.nextFruit()
+						self.timeToWait*=0.9
+						self.score+=1
+			
+		#bouge le serpent d'un pas
+		for xx in reversed(range(len(self.serpent))):
+			x=self.serpent[xx]
+			if xx==0:
+				if x.dir==SerpentAPCMini.UP:
+					x.y=(x.y+1)%8
+				elif x.dir==SerpentAPCMini.DOWN:
+					x.y=(x.y-1)%8
+				elif x.dir==SerpentAPCMini.LEFT:
+					x.x=(x.x-1)%8
+				elif x.dir==SerpentAPCMini.RIGHT:
+					x.x=(x.x+1)%8
+			else:
+				y=self.serpent[xx-1]
+				x.x=y.x
+				x.y=y.y
+				x.dir=y.dir
+	
+	
+	"""
+	Affiche l'écran de Game Over:
+	TODO: Permettre de passer l'écran en appuyant sur une touche
+	"""
+	def gameOver(self):
+		self.stop=False
+		self.changed=False
+		
+		# etape 1 affichage score et game over
+		txt = Text("  Score:"+str(self.score)+" Game Over! ", (8,4), (0,2))
+		txt.setColor(LedButton.RED)
+		self.fill(LedButton.RED_BLINK)
+		time.sleep(2)
+		self.clear()
+		for x in range(txt.stepCount()):
+			self.showText(txt)
+			if self.pollInput()!=None:	break
+			time.sleep(0.2)
+			
+		
+		# etape 2 affichage score et try again, avec les input pour
+		# quitter ou recommencer 
+		txt = Text("  Score:"+str(self.score)+" Try Again? ", (8,4), (0,4))
+		txt.setColor(LedButton.YELLOW)
+		yes=Text.letterVector('Y', LedButton.GREEN_BLINK)
+		no=Text.letterVector('N', LedButton.RED_BLINK)
+		self.mapSubRect(yes, (0,0), (4,4))
+		self.mapSubRect(no, (5,0), (9,4))
+		
+		i=0
+		while self.stop==False:
+			if i%4==0:
+				self.showText(txt)
+				self.printScore()
+			i=i+1
+			
+			yesIdx=[0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27]
+			for v in yesIdx:
+				if self.getInputState(v):
+					return True
+					
+			noIdx=[4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31,98]
+			for v in noIdx:
+				if self.getInputState(v):
+					return False
+			
+			time.sleep(0.05)
+			
+	"""
+	Initialise une nouvelle partie
+	Retour:
+		True: si tous les niveaux sont finis (pas de partie à charger)
+		False: si un niveau a été trouvé
+	"""
+	def __newGame(self):	
+		self.score=0
+		self.stop=False
+		self.loose=False
+		self.serpent=[]
+		self.fruits=[]
+		if self.levelIndex >= len(self.levels):
+			return True
+		self.obstacle=obstacleFromMatrix(self.levels[self.levelIndex]['obstacles'])
+		self.serpent.append(SerpentCase(4,3,SerpentAPCMini.RIGHT))
+		self.serpent.append(SerpentCase(3,3,SerpentAPCMini.RIGHT))
+		self.serpent.append(SerpentCase(2,3,SerpentAPCMini.RIGHT))
+		self.fruits.append(self.nextFruit())
+		self.timeToWait=self.levels[self.levelIndex]['speeds'][self.score]
+		return False
+	
+	
+	
+	"""
+	Démarre le jeu, cette fonction boucle durant tout le jeu sur elle même
+	TODO: Pouvoir recommencer ou arreter quand on gagne le jeu
+	"""
+	def start(self):
+		continuer=True
+		nextLevel=False
+		while continuer:
+			#if self.levelIndex<len(self.levels): ## gagné
+			#	pass
+			#else:
+			nextLevel=False
+			
+			self.__newGame()
+				
+			self.lastTime=time.time()
+			self.clear()
+			txt = Text("  Level "+str(self.levelIndex+1)+" ", (8,4), (0,2))
+			for x in range(txt.stepCount()-2):
+				self.showText(txt)
+				if self.pollInput()!=None:	break
+				time.sleep(0.1)
+			
+			self.fill(LedButton.GREEN_BLINK)
+			time.sleep(2)
+			
+			while self.loose==False and self.stop==False:
+				while self.timeToWait+self.lastTime>time.time() and self.changed==False:
+					time.sleep(0.001)
+					
+				self.step()
+				self.refresh()
+				self.printScore()
+				
+				if self.collision():
+					break
+				self.changed=False
+				
+				if self.score>=len(self.levels[self.levelIndex]['speeds']):
+					self.levelIndex+=1
+					nextLevel=True
+					break
+					
+				self.lastTime=time.time()
+			if not nextLevel:
+				continuer=self.gameOver()
+				self.clear()
+		return False
+
+

+ 5 - 0
simplemidi/__init__.py

@@ -0,0 +1,5 @@
+#!/usr/bin/python
+#from simplemidi.midiparser import MidiFile
+#from simplemidi.midiio import MidiInputPort, MidiOutputPort
+#from simplemidi.midimessage import *
+

+ 197 - 0
simplemidi/alsaconnector.py

@@ -0,0 +1,197 @@
+#!/usr/bin/python
+
+
+from pyalsa import alsaseq
+import time
+
+def reverse(s):
+	out=""
+	for i in reversed(range(len(s))):
+		out+=s[i]
+	return out
+
+
+SND_SEQ_PORT_CAP_READ =(1<<0)
+SND_SEQ_PORT_CAP_WRITE=(1<<1)
+SND_SEQ_PORT_CAP_SYNC_READ=(1<<2)
+SND_SEQ_PORT_CAP_SYNC_WRITE=(1<<3)
+SND_SEQ_PORT_CAP_DUPLEX=(1<<4)
+SND_SEQ_PORT_CAP_SUBS_READ =(1<<5)
+SND_SEQ_PORT_CAP_SUBS_WRITE=(1<<6)
+SND_SEQ_PORT_CAP_NO_EXPORT=(1<<7)
+
+class AlsaPort:
+	def __init__(self, clientName, name=None, clientId=None, pid=None):
+		self.clientName=clientName
+		self._update(name, clientId, pid)
+	
+	def update(self):
+		self.update()
+	
+	def _update(self, name=None, clientId=None, pid=None):
+		clientName=self.clientName
+		if name==None:
+			x = AlsaConnector.extractFromAlsaCompleteName(clientName)
+			self.clientName = x[0]
+			self.name = x[1]
+			self.clientId = x[2][0]
+			self.portId = x[2][1]
+		else:
+			self.clientName=clientName
+			self.name=name
+			self.clientId=clientId
+			self.portId=pid
+		self.globalId=(self.clientId, self.portId)
+		self.globalString=self.clientName+":"+self.name+" "+str(self.clientId)+":"+str(self.portId)
+		x=AlsaConnector.connector.get_port_info(self.portId, self.clientId)
+		self.capability=x['capability']
+		self.type=x['type']
+		self.isInput()
+		self.isOutput()
+	
+	def isInput(self):
+		return self.capability & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_DUPLEX)
+	
+	def isOutput(self):
+		return self.capability & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_DUPLEX)
+	
+	def __str__(self):
+		x = " (" +( "In" if self.isInput() else "")
+		x += ( " Out" if self.isOutput() else "")
+		return self.globalString+x+")"
+		
+		
+class AlsaClient:
+	def __init__(self, clientName, clientId, portList):
+		self.clientName=clientName
+		self.clientId=clientId
+		self.ports={}
+		for p in portList:
+			self.ports[p[1]]=AlsaPort(clientName, p[0], clientId, p[1])
+	
+	def __str__(self):
+		return self.clientName+":"+str(self.clientId)
+		
+
+class AlsaConnector:
+	def __init__(self):
+		AlsaConnector.connector=None
+		if AlsaConnector.connector==None:
+			AlsaConnector.connector=alsaseq.Sequencer()
+		
+		
+	@staticmethod
+	def listClients():
+		t=time.time()
+		out={}
+		for item in AlsaConnector.connector.connection_list():
+			clientName=item[0]
+			clientId=item[1]
+			portLists=item[2]
+			out[clientId]=AlsaClient(clientName, clientId, portLists)
+		return out
+		
+	@staticmethod
+	def listPorts():
+		out=[]
+		x=AlsaConnector.listClients()
+		for i in x:
+			for j in x[i].ports:
+				out.append(x[i].ports[j])
+		return out
+		
+	@staticmethod
+	def listPortsString():
+		out=[]
+		x=AlsaConnector.listClients()
+		for i in x:
+			for j in x[i].ports:
+				out.append(j.globalString)
+		return out
+	
+	@staticmethod
+	def connect( fro, to):
+		AlsaConnector.connector.connect_ports(fro, to)
+	
+	@staticmethod
+	def disconnect( fro, to):
+		AlsaConnector.connector.diconnect_ports(fro, to)
+	
+	@staticmethod
+	def findMe(cname, pname):
+		l=AlsaConnector.listPorts()
+		for xi in reversed(range(len(l))):
+			x=l[xi]
+			name=x.globalString
+			lastSpace=name.rfind(' ')
+			if lastSpace<0: return None
+			name=name[:lastSpace]
+			if name == (cname+":"+pname):
+				return x
+		return None
+			
+		
+	@staticmethod
+	def getPortFromId(alsaId):
+		clients=AlsaConnector.listClients()
+		if alsaId[0] in clients: 
+			if alsaId[1] in  clients[alsaId[0]].ports:
+				return clients[alsaId[0]].ports[alsaId[1]]
+		return None
+		
+	@staticmethod
+	def getPortNameFromId(alsaId):
+		clients=AlsaConnector.listClients()
+		if alsaId[0] in clients: 
+			if alsaId[1] in  clients[alsaId[0]].ports:
+				return clients[alsaId[0]].ports[alsaId[1]].globalString
+		return None
+	
+	@staticmethod
+	def portAuto(port):
+		if isinstance(port, (tuple, list)):
+			port=AlsaConnector.getPortFromId(port)
+		elif isinstance(port, str):
+			port=AlsaConnector.getPortFromId(port)
+		return port
+		
+	@staticmethod
+	def getPortFromName(name):
+		return extractFromAlsaCompleteName(name)
+	
+	@staticmethod
+	def extractFromAlsaCompleteName(name):
+		clientName=""
+		portName=""
+		clientId=0
+		portId=0
+		i=len(name)-1
+		tmp=""
+		
+		#port id
+		while name[i]!=':':
+			tmp+=name[i]
+			i-=1
+		i-=1
+		portId=int(reverse(tmp))
+		
+		
+		tmp=""
+		while name[i]!=' ':
+			tmp+=name[i]
+			i-=1
+		clientId=int(reverse(tmp))
+		i-=1
+		
+		tmp=""
+		while name[i]!=':':
+			tmp+=name[i]
+			i-=1
+
+		portName=reverse(tmp)
+		clientName=name[:i]
+
+		return (clientName, portName, (clientId, portId))
+	
+		
+__alsa_connector_singleton=AlsaConnector()

+ 141 - 0
simplemidi/midieventtrigger.py

@@ -0,0 +1,141 @@
+#!/usr/bin/python
+
+import copy
+from .midimessage import MidiType
+from .utils import *
+
+class MidiEventTrigger:
+	NOTE=1
+	CONTROL=2
+	PROGRAM=4
+	FILTER_ALL_KEY=-1
+	
+	def __init__(self, param):
+		if param==None: self._initNone()
+		elif isinstance(param, (tuple, list)): self._initTuple(param)
+		elif isinstance(param, int): self._initTuple([param])
+		elif isinstance(param, MidiEventTrigger): self._initCopy(param)
+		
+
+
+	def _initNone(self):
+		self._init()
+		
+	"""
+	() -> déclenchement pour toutes les evenemnts parmis NOTE, CONTROL et PROGRAM
+	(TYPE)
+	(TYPE, CHANNEL, NOTE)
+	(TYPE, CHANNEL, NOTE, FORWARD)
+	(TYPE, CHANNEL, NOTE, FORWARD, CALLBACK [, DATA])
+	TYPE: flags parmis NOTE, CONTROL, PROGRAM (qui sera une condition necessaire au déclenchement)
+	CHANNEL: le canal pour le déclenchement
+		none ou vide: declenchement pour toutes les valeurs
+		int: declenchement pour une valeur
+		array: declenchement pour toutes les valeurs de la liste
+		tuple: declenchement pour l'interval (x,y) avec x ET y inclus 
+	NOTE: la clé de la Note du Program ou Control pour le déclenchement
+		none ou vide: declenchement pour toutes les valeurs
+		int: declenchement pour une valeur
+		array: declenchement pour toutes les valeurs de la liste
+		tuple: declenchement pour l'interval (x,y) avec x ET y inclus 
+	FORWARD: Uen fois l'élément traité doit il etre réinjecter en canal de sortie ?
+		True ou vide: Oui
+		None ou False: Non
+	CALLBACK et DATA:
+		callback appelé en cas de céclenchement de la forem fct(msg, data)
+	"""
+	def _initTuple(self, t):
+		self._init(*t)
+		
+		
+		
+	def _initCopy(self, t):
+		_init(t.type, copy.copy(t.channel), copy.copy(t.key), t.forward)
+		
+		
+	def _init(self, t=NOTE|CONTROL|PROGRAM, ch=None, k=None, f=True, cb=None, data=None ):
+		self.channel=self.FILTER_ALL_KEY
+		self.key=self.FILTER_ALL_KEY
+		self.forward=True
+		
+		self.type=t  
+		if isinstance(ch, list): 
+			self.channel=ch
+		elif isinstance(ch, tuple): 
+			self.channel=[]
+			for i in range(ch[0], ch[1]+1):
+				self.channel.append(i)
+		if isinstance(k, list): 
+			self.key=k
+		elif isinstance(k, tuple): 
+			self.key=[]
+			for i in range(k[0], k[1]+1):
+				self.key.append(i)
+		self.forward=f
+		self.callback=cb
+		self.callbackData=data
+		
+	def trigger(self, msg):
+		if self.callback:
+			self.callback(msg, self.callbackData)
+	
+	def filter(self, _msg, out):
+		msg=_msg[1]
+		delta=_msg[0]
+		t=msg.getType()
+		if t!=MidiType.NOTE_ON and t!=MidiType.NOTE_OFF and \
+			t!=MidiType.PROGRAM_CHANGE and t!=MidiType.CONTROL_CHANGE: return False
+		
+		#test du type
+		lt=[]
+		if self.type & self.NOTE: lt+=[MidiType.NOTE_ON, MidiType.NOTE_OFF]
+		if self.type & self.CONTROL: lt+=[MidiType.CONTROL_CHANGE]
+		if self.type & self.PROGRAM: lt+=[MidiType.PROGRAM_CHANGE]
+		ok=False
+		for tt in lt:
+			if tt==t:
+				ok=True
+				break
+		if not ok: return False
+		
+		if self.key!=None and self.key!=-1 and not (msg.key in self.key): return False
+		if self.channel!=None and self.channel!=-1 and not (msg.key in self.channel): return False
+		if out!=None and self.forward: out.send(msg)
+		print(msg)
+		self.trigger(msg)
+		return True
+		
+		
+class MidiMultiTrigger:
+	
+	"""
+		param={
+			id_1 : PARAM_TO_MIDI_EVENT_TRIGGER,
+			id_2 : PARAM_TO_MIDI_EVENT_TRIGGER,
+			...
+			id_n : PARAM_TO_MIDI_EVENT_TRIGGER,
+		}
+	"""
+	def __init__(self, param={}):
+		if not isinstance(param, dict): raise Exception("MidiMultiTrigger expects an dict argument")
+		self.data={}
+		for i in param.keys():
+			self.addTrigger(i, param[i])
+	
+	
+	def addTrigger(self, name, param):
+		self.data[name]=MidiEventTrigger(param)
+	
+	"""
+	names: None ou une liste des id a tester
+	renvoie une liste des declenchements
+	"""
+	def filter(self, msg, out, names=None):
+		ret=[]
+		toTest=self.data.keys()
+		if names!=None: toTest=names
+		
+		for i in toTest:
+			if self.data[i].filter(msg, out): ret.append(i)
+		return ret
+

+ 519 - 0
simplemidi/midiio.py

@@ -0,0 +1,519 @@
+#!/usr/bin/python
+
+from .midimessage import *
+from rtmidi import MidiIn
+from rtmidi import MidiOut
+from rtmidi import MidiOut
+import copy
+import time
+from .alsaconnector import AlsaConnector, AlsaPort
+from .options import *
+from .utils import *
+
+ALSA = AlsaConnector()
+
+class MIDIPortException(Exception):
+	def __init__(self, message):
+		super().__init__(message)
+
+def _MidiPort__input_pad_error_cb(err, msg, obj):
+	obj.inputError(err, msg)
+
+class _MidiPort():
+	_DEFAULT_PARAMS={
+		'port': None,
+		'port_name' : "Midi Port",
+		'client_name' : "Simple MIDI",
+		'error_callback' : None
+	}
+	
+	
+	def __init__(self, direction,params):
+		param=initParams(_MidiPort._DEFAULT_PARAMS, params)
+		_MidiPort.INPUT=0
+		_MidiPort.OUTPUT=1
+		
+		
+		self.direction=direction
+		self.port=param['port']
+		self.portName=param['port_name']
+		self.clientName=param['client_name']
+		self.errorCb=param['error_callback']
+		
+
+		if self.port==None:
+			if direction==_MidiPort.INPUT:
+				self.port=MidiIn()
+			else:
+				self.port=MidiOut()
+				
+		self.port.set_error_callback(__input_pad_error_cb, self)
+		self.alsaPort=None
+	
+	
+	def _updateAlsaPort(self):
+		cid=self.alsaPort.clientId
+		pid=self.alsaPort.portId
+		self.alsaPort=AlsaConnector.getPortFromId((cid, pid))
+		self.portName=self.alsaPort.name
+		
+	
+	def _initClientId(self, cid, pid):
+		x=None
+		if self.direction==_MidiPort.INPUT:
+			x=MidiOut()
+		else:
+			x=MidiIn()
+		self.alsaPort = AlsaConnector.findMe(cid, pid)
+				
+	def inputError(self, err, msg):
+		ERR("MIDI Error :"+type(err).__name__+" "+msg)
+		if self.errorCb:
+			self.errorCb[0](self.errorCb[1], err, msg)
+		
+	def __input_pad_error_cb(err, msg):
+		if self.errorCb:
+			self.errorCb[0](self.errorCb[1], err, msg)
+		
+	def setErrorCallback(self, fct, data):
+		self.errorCb=(fct,data)
+	
+	def cancelErrorCallback(self):
+		self.errorCb=None
+	
+	def setClientName(self, name):
+		self.clientName=name.replace(':', ' ')
+		self.port.set_client_name(self.clientName)
+		if self.isOpen(): self._updateAlsaPort()
+		
+	def setPortName(self, name):
+		self.portName=name.replace(':', ' ')
+		self.portName=name
+		self.port.set_port_name(self.portName)
+		if self.isOpen(): self._updateAlsaPort()
+		
+	def getPortName(self, n=None):
+		if n==None:
+			return self.portName
+		return self.port.get_port_name(n)
+	
+	def isOpen(self):
+		return self.port.is_port_open()
+	
+	def getPorts(self):
+		return self.port.get_ports()
+		
+	def getPortCount(self):
+		return self.port.get_port_count()
+	
+	def getApi(self):
+		return self.port.get_current_api()
+	
+	def open(self, port=None):
+		if self.isOpen():
+			return True
+		if port==None:
+			port=self.portName
+			
+		self.setClientName(self.clientName)
+		self.port.open_virtual_port(port)
+		if self.isOpen():
+			self._initClientId(self.clientName,port)
+			INFO("ALSA Midi port: "+str(self.alsaPort)+" created")
+			return True
+		return False
+	
+	def connect(self, to):
+		if isinstance(to, tuple):
+			if self.direction==_MidiPort.OUTPUT:
+				DEBUG("Connecting  '"+str(self.alsaPort)+"' to "+str(to)+" ...")
+				ALSA.connect(self.alsaPort.globalId, to)
+			else:
+				DEBUG("Connecting  '"+str(to)+"' to "+str(self.alsaPort)+" ...")
+				ALSA.connect(to, self.alsaPort.globalId)
+		elif isinstance(to, str):
+			port=AlsaPort(to)
+			return self.connect(port.globalId)
+		elif isinstance(to, int):
+			return self.connect(self.getPorts()[to])
+		elif isinstance(to, AlsaPort):
+			return self.connect(to.globalId)
+		
+	
+	def disconnect(self, to):
+		if self.direction==_MidiPort.INPUT:
+			ALSA.disconnect(to, self.portId)
+		else:
+			ALSA.disconnect(self.portId, to)
+	
+	
+	def close(self):
+		self.port.close_port()
+		return not self.isOpen()
+
+
+	
+"""
+#
+# Input
+#
+#
+"""		
+
+def _on_input_callback(data, obj):
+	obj._on_input_callback(data)
+	
+class MidiInputPort(_MidiPort):
+	
+	_DEFAULT_PARAMS= dictAssign(_MidiPort._DEFAULT_PARAMS,{
+		'mode': 0,
+		'ignore_midi_type' : {},
+		'separate_callback': {},
+		'input_callback': None
+	})
+	
+	
+	@staticmethod
+	def fromParams(param):
+		param=initParams(MidiInputPort._DEFAULT_PARAMS, param)
+		if param['port']==None:
+			return MidiInputPort(param) 
+		else:
+			return param
+
+	
+	def __init__(self, params):
+		param=initParams(MidiInputPort._DEFAULT_PARAMS, params)
+		_MidiPort.__init__(self, 0, param)
+		MidiInputPort.MODE_QUEUE=0
+		MidiInputPort.MODE_CALLBACK=1
+		self.port.set_callback(_on_input_callback, self)
+		self.port.ignore_types(False,False,False)
+		self.queue=Queue()
+		
+		self.inputCb=param['input_callback']
+		self.separatesCB=param['separate_callback']
+		self.mode=param['mode']
+		self.ignore=param['ignore_midi_type']
+		
+	"""
+		Modes
+	"""
+	def setMode(self, mode):
+		if self.mode!=MidiInputPort.MODE_QUEUE and \
+			self.mode!=MidiInputPort.MODE_CALLBACK:
+				raise MIDIPortException("Bad input mode ("+str(self.mode)+")")
+		self.mode=mode
+		
+	def getMode(self):
+		return self.mode
+	
+	
+	"""
+		Callbacks
+	"""
+	def setInputCallback(self, fct, data, typ=None):
+		self.mode=MidiInputPort.MODE_CALLBACK
+		if typ==None:
+			self.inputCb=(fct,data)
+		else:
+			self.separatesCB[typ]=(fct, data)
+	
+	def cancelInputCallback(self, typ=None):
+		if typ==None:
+			self.inputCb=None
+		else:
+			self.separatesCB[typ]=None
+		if len(self.separatesCB.keys())==0 and self.inputCb==None:
+			self.mode=MidiInputPort.MODE_QUEUE
+	
+	def cancelAllCallbacks(self):
+		self.inputCb=None	
+		self.separatesCB={}
+		self.mode=MidiInputPort.MODE_QUEUE
+	
+	def _on_input_callback(self, data):
+		t=data[1]
+		obj=MidiMessage.parse(data[0])
+		typ=obj.getType()
+		if self.mode==MidiInputPort.MODE_QUEUE:
+			if not ((typ in self.ignore.keys()) and self.ignore[typ]==True):
+				self.queue.enqueue( (t,obj) )
+		elif self.mode==MidiInputPort.MODE_CALLBACK:
+			if self.inputCb and self.inputCb[0]:
+				self.inputCb[0]( t, obj)
+			if typ in self.separatesCB:
+				cb=self.separatesCB[typ]
+				if cb[0]:
+					cb[0](t, obj)
+	
+	"""
+		Others
+	"""
+	def get(self, sync=False):
+		if sync: return self.getSync()
+		if self.mode==MidiInputPort.MODE_QUEUE:
+			x=self.queue.peek()
+			if x: self.queue.dequeue()
+			return x
+		else:
+			raise MIDIPortException("Bad mode for : get")
+		
+	def getSync(self):
+		return self.queue.dequeue()
+		
+	def ignoreMessages(self, data):
+		for k in data.keys():
+			self.ignore[k]=data[k]
+"""
+MIDIInputPort.ALL_IGNORED={ # True event is skipped, False or undefinedevent pass
+		MidiType.NOTE_ON:True,
+		MidiType.NOTE_OFF:True,
+		MidiType.KEY_PRESSURE:True,
+		MidiType.CONTROL_CHANGE:True,
+		MidiType.PROGRAM_CHANGE:True,
+		MidiType.CHANNEL_PRESSURE:True,
+		MidiType.PITCH_WHEEL_CHANGE:True,
+		MidiType.SYSTEM_EXCLUSIVE:True,
+		MidiType.SONG_POSITION_POINTER:True,
+		MidiType.SONG_SELECT:True,
+		MidiType.TUNE_REQUEST:True,
+		MidiType.END_OF_EXCUSIVE:True,
+		MidiType.TIMING_CLOCK:True,
+		MidiType.START:True,
+		MidiType.CONTINUE:True,
+		MidiType.STOP:True,
+		MidiType.ACTIVE_SENSING:True,
+		MidiType.META:True,
+		MidiType.RESET:True,
+		MidiType.META_SEQ:True,
+		MidiType.META_TEXT:True,
+		MidiType.META_COPYRIGHT:True,
+		MidiType.META_TRACK_NAME:True,
+		MidiType.META_INSTRUMENT_NAME:True,
+		MidiType.META_LYRICS:True,
+		MidiType.META_TEXT_MARKER:True,
+		MidiType.META_CUE_POINT:True,
+		MidiType.META_CHANNEL_PREFIX:True,
+		MidiType.META_END_OF_TRACK:True,
+		MidiType.META_SET_TEMPO:True,
+		MidiType.META_SMPTE_OFFSET:True,
+		MidiType.META_TIME_SIGNATURE:True
+}
+"""
+
+
+
+"""
+#
+# Output
+#
+#
+"""
+class MidiOutputPort(_MidiPort):
+	_DEFAULT_PARAMS=_MidiPort._DEFAULT_PARAMS
+	
+	
+	@staticmethod
+	def fromParams(param):
+		param=initParams(MidiOutputPort._DEFAULT_PARAMS, param)
+		if param['port']==None:
+			return MidiOutputPort(param) 
+		else:
+			return param
+	
+	def __init__(self, params):
+		_MidiPort.__init__(self,  1, params)
+	
+	def send(self, param):
+		if isinstance(param, (bytes, list)):
+			send_message(self.port, param)
+			#self.port.send_message(param)
+		else:
+			send_message(self.port, param.bytes())
+			#self.port.send_message(param.bytes())
+	
+	def noteOn(self, ch, key, vel):
+		self.send(NoteOn.new(ch, key, vel).bytes())
+	
+	def noteOff(self, ch, key, vel):
+		self.send(NoteOff.new(ch, key, vel).bytes())
+	
+	def keyPressure(self, ch, key, pressure):
+		self.send(KeyPressure.new(ch, key, pressure).bytes())
+		
+	def controlChange(self, ch, key, value):
+		self.send(ControlChange.new(ch, key, value).bytes())
+		
+	def programChange(self, ch, key):
+		self.send(ProgramChange.new(ch, key).bytes())
+		
+	def channelPressure(self, ch, pressure):
+		self.send(ChannelPressure.new(ch, pressure).bytes())
+		
+	def pitchWheelChange(self, ch, pressure):
+		self.send(PitchWheelChange.new(ch, pressure).bytes())
+		
+	def systemExclusive(self, data):
+		self.send(SystemExclusive.new(data).bytes())
+		
+	def songPointerPosition(self, beats):
+		self.send(SongPointerPosition.new(beats).bytes())
+		
+	def songSelect(self, song):
+		self.send(SongSelect.new(song).bytes())
+		
+	def tuneRequest(self):
+		self.send(TuneRequest.new().bytes())
+		
+	def timingClock(self):
+		self.send(TimingClock.new().bytes())
+		
+	def start(self):
+		self.send(Start.new().bytes())
+		
+	def continuer(self):
+		self.send(Continue.new().bytes())
+		
+	def stop(self):
+		self.send(Stop.new().bytes())
+		
+	def activeSensing(self):
+		self.send(ActiveSensing.new().bytes())
+		
+	def endOfTrack(self):
+		self.send(EndOfTrack.new().bytes())
+		
+	def sequenceNumber(self):
+		self.send(SequenceNumber.new().bytes())
+		
+	def textEvent(self, text):
+		self.send(TextEvent.new(text).bytes())
+		
+	def copyright(self, text):
+		self.send(Copyright.new(text).bytes())
+		
+	def trackName(self, text):
+		self.send(TrackName.new(text).bytes())
+		
+	def instrumentName(self, text):
+		self.send(InstrumentName.new(text).bytes())
+		
+	def textLyrics(self, text):
+		self.send(TextLyrics.new(text).bytes())
+		
+	def textMarker(self, text):
+		self.send(TextMarker.new(text).bytes())
+		
+	def textEvent(self, text):
+		self.send(TextEvent.new(text).bytes())
+		
+	def cuePoint(self, text):
+		self.send(CuePoint.new(text).bytes())
+		
+	def channelPrefix(self, cc):
+		self.send(ChannelPrefix.new(cc).bytes())
+		
+	def setTempo(self, tempo):
+		self.send(SetTempo.new(tempo).bytes())
+		
+	def SMPTEOffset(self, hh, mm, ss, fr, ff):
+		self.send(SMPTEOffset.new(hh, mm, ss, fr, ff).bytes())
+		
+	def timeSignature(self, numerator, denominator, clic, notes):
+		self.send(SMPTEOffset.new(numerator, denominator, clic, notes).bytes())
+		
+
+if __name__=="__main__":
+	ipp=MidiInputPort()
+	ipp.setClientName("Test 1")
+	ipp.open("d")
+	ippp=MidiInputPort()
+	ippp.setClientName("Test 1")
+	ippp.open("c")
+	
+	op=MidiOutputPort()
+	op.setClientName("Test 1")
+	op.open("b")
+	opp=MidiOutputPort()
+	opp.setClientName("Test 1")
+	opp.open("a")
+	
+	
+	time.sleep(100)
+
+
+
+"""
+[
+	('System', 0, 
+		[
+			('Timer', 0, ([], [])), 
+			('Announce', 1, 
+				(
+					[
+						(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0}), 
+						(130, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0})
+					], 
+					[]
+				)
+			)
+		]
+	)
+	,('Midi Through', 14, 
+		[
+			('Midi Through Port-0', 0, 
+				(
+					[(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 1, 'time_real': 1})], 
+					[(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0})]
+				)
+			)
+		]
+	), ('APC MINI', 28, 
+		[
+			('APC MINI MIDI 1', 0, 
+				(
+					[(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 1, 'time_real': 1})], 
+					[(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0})]
+				)
+			)
+		]
+	), ('a2jmidid', 128, 
+		[
+			('port', 0, 
+				(
+					[(14, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0}), (28, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0}), (129, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0})], 
+					[(0, 1, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0}), (14, 0, {'queue': 0, 'exclusive': 0, 'time_update': 1, 'time_real': 1}), (28, 0, {'queue': 0, 'exclusive': 0, 'time_update': 1, 'time_real': 1}), (129, 1, {'queue': 0, 'exclusive': 0, 'time_update': 1, 'time_real': 1})]
+				)
+			)
+		]
+	), ('mididings', 129, 
+		[
+			('in_1', 0, 
+				(
+					[], 
+					[(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0})]
+				)
+			),
+			('out_1', 1, 
+				(
+					[(128, 0, {'queue': 0, 'exclusive': 0, 'time_update': 1, 'time_real': 1})], 
+					[]
+				)
+			)
+		]
+	), ('Patchage', 130, 
+		[
+			('System Announcement Reciever', 0,
+				(
+					[], 
+					[(0, 1, {'queue': 0, 'exclusive': 0, 'time_update': 0, 'time_real': 0})]
+				)
+			)
+		]
+	), ('pyalsa-10554', 131, 
+		[
+		]
+	)
+]
+"""

+ 873 - 0
simplemidi/midimessage.py

@@ -0,0 +1,873 @@
+#!/usr/bin/python
+
+from .options import *
+import time
+import io
+
+class MIDIParserException(Exception):
+	def __init__(self, message):
+		super().__init__(message)
+
+
+def send_message(x, data):
+	s="["
+	for i in range(len(data)):
+		if i>0: s+=", "
+		s+=hex(data[i])
+	MIDIDEBUG(s+"]")
+	x.send_message(data)
+
+class MidiType:
+	def __init__(self):
+		MidiType.ERROR=0
+		MidiType.NOTE_OFF=0x80
+		MidiType.NOTE_ON=0x90
+		MidiType.KEY_PRESSURE=0xA0
+		MidiType.CONTROL_CHANGE=0xB0
+		MidiType.PROGRAM_CHANGE=0xC0
+		MidiType.CHANNEL_PRESSURE=0xD0
+		MidiType.PITCH_WHEEL_CHANGE=0xE0
+		
+		MidiType.SYSTEM_EXCLUSIVE=0xF0
+		MidiType.SONG_POSITION_POINTER=0xF2
+		MidiType.SONG_SELECT=0xF2
+		MidiType.TUNE_REQUEST=0xF6
+		MidiType.END_OF_EXCUSIVE=0xF7 #not used
+		
+		MidiType.TIMING_CLOCK=0xF8 # a faire
+		MidiType.START=0xFA
+		MidiType.CONTINUE=0xFB
+		MidiType.STOP=0xFC
+		MidiType.ACTIVE_SENSING=0xFE
+		MidiType.META=0xFF
+		MidiType.RESET=0xFF
+		
+		MidiType.META_SEQ=0x00
+		MidiType.META_TEXT=0x01
+		MidiType.META_COPYRIGHT=0x02
+		MidiType.META_TRACK_NAME=0x03
+		MidiType.META_INSTRUMENT_NAME=0x04
+		MidiType.META_LYRICS=0x05
+		MidiType.META_TEXT_MARKER=0x06
+		MidiType.META_CUE_POINT=0x07
+		
+		MidiType.META_CHANNEL_PREFIX=0x20
+		MidiType.META_END_OF_TRACK=0x2F
+		MidiType.META_SET_TEMPO=0x51
+		MidiType.META_SMPTE_OFFSET=0x54
+		MidiType.META_TIME_SIGNATURE=0x58
+		MidiType.META_KEY_SIGNATURE=0x59
+		MidiType.META_SPECIFIC=0x7F
+		
+		
+	
+MidiType()
+
+
+class MidiMessage:
+	def __init__(self, typ):
+		self.type=typ
+		self.__sleep=0.00001
+		self.__doSleep=True
+		self.key=-1
+	
+	def isEndTrack(self):
+		return False
+	
+	def getType(self):
+		if self.type>=0x80 and self.type<=0xE0:
+			return self.type&0xf0
+		return self.type
+		
+	def isTypeOf(self, x):
+		return self.getType() == x
+		
+	def isMeta(self):
+		return  self.type>=0 and self.type<=0x58
+	
+	def isSysEx(self):
+		return  self.type>=0xF0 and self.type<=0xF7
+		
+	def write(self, oport):
+		send_message(oport, self.bytes())
+		#oport.send_message(self.bytes())
+		if self.__doSleep:
+			time.sleep(self.__sleep)
+	
+	def read(self, iport, n=1):
+		if n>0:
+			out=[]
+			for i in range(n):
+				out.append(self.byte())
+			return out
+		
+		return iport.get_message()
+	
+	def syncRead(self, iport):
+		x=None
+		while True:
+			x=self.read(iport)
+			if x!=None:
+				return x
+	
+	def __eq__(self, x):
+		
+		xx=x.bytes()
+		ex=self.bytes()
+		
+		if len(xx) != len(ex): return False
+		for i in len(xx):
+			if xx[i] != ex[i]:
+				return False
+		return True
+	
+	def transpose(self, n):
+		pass
+	
+	def copy(self):
+		x=MidiMessage.parse(self.bytes())
+		return x
+	
+	@staticmethod
+	def byte(fd):
+		vv=fd.read(1)
+		if vv==None or len(vv)==0:
+			raise MIDIParserException("End off file premature")
+		return int(vv[0])
+	
+	@staticmethod
+	def parse(fd):
+		if isinstance(fd, (bytes, list)):
+			b=bytes(fd)
+			fd=io.BytesIO(b)
+		return MidiMessage.parseStream(fd)
+		
+	@staticmethod
+	def parseStream(fd):
+		vv=fd.read(1)
+		if vv==None:
+			return MidiEndFile()
+		first=int(vv[0])
+	
+		xa= first&0xf0
+		xb= first&0x0f
+		
+		if  xa==MidiType.NOTE_ON: return NoteOn(xb, fd)
+		elif xa==MidiType.NOTE_OFF: return NoteOff(xb, fd)
+		elif xa==MidiType.KEY_PRESSURE: return KeyPressure(xb, fd)
+		elif xa==MidiType.CONTROL_CHANGE: return ControlChange(xb, fd)
+		elif xa==MidiType.PROGRAM_CHANGE: return ProgramChange(xb, fd)
+		elif xa==MidiType.CHANNEL_PRESSURE: return ChannelPressure(xb, fd)
+		elif xa==MidiType.PITCH_WHEEL_CHANGE: return PitchWheelChange(xb, fd)
+		
+		elif first==MidiType.SYSTEM_EXCLUSIVE: return SystemExclusive(fd)
+		elif first==MidiType.SONG_POSITION_POINTER: return SongPointerPosition(fd)
+		elif first==MidiType.SONG_SELECT: return SongSelect(fd)
+		elif first==MidiType.TUNE_REQUEST: return TuneRequest(fd)
+		
+		elif first==MidiType.TIMING_CLOCK: return TimingClock(fd)
+		elif first==MidiType.START: return Start(fd)
+		elif first==MidiType.CONTINUE: return Continue(fd)
+		elif first==MidiType.STOP: return Stop(fd)
+		elif first==MidiType.ACTIVE_SENSING: return ActiveSensing(fd)
+		elif first==MidiType.META: return MidiMessage.parseMeta( fd)
+		else: return MidiError(first)
+	
+	@staticmethod
+	def parseMeta(fd):
+		cmd=MidiMessage.byte(fd)
+		if cmd==MidiType.META_SEQ: return SequenceNumber(fd)
+		elif cmd==MidiType.META_TEXT: return TextEvent(fd)
+		elif cmd==MidiType.META_COPYRIGHT: return Copyright(fd)
+		elif cmd==MidiType.META_TRACK_NAME: return TrackName(fd)
+		elif cmd==MidiType.META_INSTRUMENT_NAME: return InstrumentName(fd)
+		elif cmd==MidiType.META_LYRICS: return TextLyrics(fd)
+		elif cmd==MidiType.META_TEXT_MARKER: return TextMarker(fd)
+		elif cmd==MidiType.META_CUE_POINT: return CuePoint(fd)
+		elif cmd==MidiType.META_CHANNEL_PREFIX: return ChannelPrefix(fd)
+		elif cmd==MidiType.META_END_OF_TRACK: return EndOfTrack(fd)
+		elif cmd==MidiType.META_SET_TEMPO: return SetTempo(fd)
+		elif cmd==MidiType.META_SMPTE_OFFSET: return SMPTEOffset(fd)
+		elif cmd==MidiType.META_TIME_SIGNATURE: return TimeSignature(fd)
+		elif cmd==MidiType.META_KEY_SIGNATURE: return KeySignature(fd)
+		elif cmd==MidiType.META_SPECIFIC: return MetaSpecific(fd)
+		else: return MidiError(cmd, fd.offset)
+
+
+"""
+Voices Messages
+"""
+class MidiVoiceMessage(MidiMessage):
+	def __init__(self, typ, ch, fd=None):
+		MidiMessage.__init__(self, typ)
+		self.channel=ch+1
+	
+	
+
+class NoteOn(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.NOTE_ON, ch)
+		if fd:
+			self.key=self.byte(fd)
+			self.velocity=self.byte(fd)
+			
+	def bytes(self):
+		return [MidiType.NOTE_ON | (self.channel-1), self.key, self.velocity]
+	
+	@staticmethod
+	def new(chan, key, vel):
+		x=NoteOn(chan+-1)
+		x.key=key
+		x.velocity=vel
+		return x
+	
+	def transpose(self, n):
+		self.key+=n
+
+	def __str__(self):
+		return "NOTEON("+str(self.key)+", "+ str(self.channel) +")"
+
+
+
+
+class NoteOff(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.NOTE_OFF, ch)
+		if fd:
+			self.key=self.byte(fd)
+			self.velocity=self.byte(fd)
+			
+	def bytes(self):
+		return [MidiType.NOTE_OFF | (self.channel-1), self.key, self.velocity]
+		
+	@staticmethod
+	def new( chan, key, vel):
+		x=NoteOff(chan-1)
+		x.key=key
+		x.velocity=vel
+		return x
+		
+	def transpose(self, n):
+		self.key+=n
+
+
+
+
+
+
+class KeyPressure(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.KEY_PRESSURE, ch)
+		if fd:
+			self.key=self.byte(fd)
+			self.pressure=self.byte(fd)
+			
+	def bytes(self):
+		return [MidiType.KEY_PRESSURE | (self.channel-1), self.key, self.value]
+		
+	def new(self, chan, key, pressure):
+		x=KeyPressure(chan-1)
+		x.key=key
+		x.pressure=pressure
+		return x
+		
+
+
+
+
+
+
+
+class ControlChange(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.CONTROL_CHANGE, ch)
+		if fd:
+			self.key=self.byte(fd)
+			self.value=self.byte(fd)
+			
+	def bytes(self):
+		return [MidiType.CONTROL_CHANGE | (self.channel-1), self.key, self.value]
+		
+	def new(self, chan, key, value):
+		x=ControlChange(chan-1)
+		x.key=key
+		x.value=value
+		return x
+		
+
+
+
+
+
+
+class ProgramChange(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.PROGRAM_CHANGE, ch)
+		if fd:
+			self.key=self.byte(fd)
+			
+	def bytes(self):
+		return [MidiType.PROGRAM_CHANGE | (self.channel-1), self.key]
+		
+	def new(self, chan, key):
+		x=ProgramChange(chan-1)
+		x.key=key
+		return x
+		
+		
+		
+		
+		
+		
+class ChannelPressure(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.CHANNEL_PRESSURE, ch)
+		if fd:
+			self.pressure=self.byte(fd)
+			
+	def bytes(self):
+		return [MidiType.CHANNEL_PRESSURE | (self.channel-1), self.key, self.pressure]
+		
+	def new(self, chan, pressure):
+		x=ChannelPressure(chan-1)
+		x.pressure=pressure
+		return x
+		
+		
+		
+		
+		
+		
+		
+		
+class PitchWheelChange(MidiVoiceMessage):
+	def __init__(self, ch, fd=None):
+		MidiVoiceMessage.__init__(self, MidiType.PITCH_WHEEL_CHANGE, ch)
+		if fd:
+			self.pressure=self.byte(fd)
+			self.pressure+=(self.byte(fd)<<7)
+			
+	def bytes(self):
+		return [MidiType.PITCH_WHEEL_CHANGE | (self.channel-1), 
+			self.pressure & 0x7f, (self.pressure & 0x3f8)>>7]
+			
+	def new(self, chan, pressure):
+		x=PitchWheelChange(chan-1)
+		x.pressure=pressure
+		return x
+		
+		
+		
+		
+		
+		
+		
+		
+"""
+System Common Messages
+"""
+		
+class SystemExclusive(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.SYSTEM_EXCLUSIVE)
+		if fd:
+			self.data=[]
+			while True:
+				x=self.byte(fd)
+				if x==MidiMessage.END_OF_EXCUSIVE:
+					break
+				else:
+					self.data.append(x)
+			
+	def bytes(self):
+		return [MidiType.SYSTEM_EXCLUSIVE]+self.data+[MidiType.END_OF_EXCUSIVE]
+			
+	def new(self, data):
+		x=SystemExclusive(chan)
+		x.data=data
+		return x
+				
+				
+				
+				
+				
+				
+				
+class SongPointerPosition(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.SONG_POSITION_POINTER)
+		if fd:
+			self.beats=self.byte(fd)
+			self.beats+=(self.byte(fd)<<7)
+		
+	def bytes(self):
+		return [MidiType.SONG_POSITION_POINTER, self.beats & 0x7f, (self.beats & 0x3f8)>>7]
+			
+	def new(self, beats):
+		x=SongPointerPosition(chan)
+		x.beats=beats
+		return x
+		
+
+
+
+
+
+
+
+
+class SongSelect(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.SONG_SELECT)
+		if fd:
+			self.song=fd.self.byte(fd)
+		
+	def bytes(self):
+		return [MidiType.SONG_SELECT, self.song]
+		
+	def new(self, song):
+		x=SongSelect(chan)
+		x.song=song
+		return x
+		
+		
+		
+		
+		
+		
+		
+		
+class TuneRequest(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.TUNE_REQUEST)
+		
+	def bytes(self):
+		return [MidiType.TUNE_REQUEST]
+		
+	def new(self):
+		return TuneRequest()
+		
+		
+		
+		
+		
+		
+		
+		
+		
+"""
+System RT Messages
+"""
+	
+class TimingClock(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.TimingClock)
+		
+	def bytes(self):
+		return [MidiType.TIMING_CLOCK]
+		
+	def new(self):
+		return TimingClock()
+	
+	
+	
+	
+	
+	
+	
+class Start(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.START)
+		
+	def bytes(self):
+		return [MidiType.START]
+		
+	def new(self):
+		return Start()
+	
+	
+	
+	
+	
+	
+	
+	
+	
+class Continue(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.CONTINUE)
+		
+	def bytes(self):
+		return [MidiType.CONTINUE]
+		
+	def new(self):
+		return Continue()
+	
+	
+	
+	
+	
+	
+	
+class Stop(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.STOP)
+		
+	def bytes(self):
+		return [MidiType.STOP]
+		
+	def new(self):
+		return Stop()
+	
+	
+	
+	
+	
+	
+class ActiveSensing(MidiMessage):
+	def __init__(self, fd=None):
+		MidiMessage.__init__(self, MidiType.ACTIVE_SENSING)
+		
+	def bytes(self):
+		return [MidiType.ACTIVE_SENSING]
+		
+	def new(self):
+		return ActiveSensing()
+	
+	
+	
+	
+	
+		
+		
+"""
+Meta
+"""
+class Meta(MidiMessage):
+	def __init__(self, cmd):
+		MidiMessage.__init__(self, MidiType.META)
+		self.command=cmd
+
+
+
+
+
+
+
+class EndOfTrack(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_END_OF_TRACK)
+		if fd:
+			self.byte(fd)
+	
+	def isEndTrack(self):
+		return True      
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 0]
+		
+	def new(self):
+		return EndOfTrack()
+
+
+
+
+
+class SequenceNumber(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_SEQ)
+		if fd:
+			self.number=self.byte(fd)
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 0x2]
+		
+	def new(self):
+		return SequenceNumber()
+		
+		
+		
+		
+		
+		
+		
+class TextEvent(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_TEXT)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=TextEvent()
+		self.text=text
+		return x
+		
+		
+		
+		
+		
+		
+class Copyright(Meta):
+	def __init__(self,cmd, fd=None):
+		Meta.__init__(self, MidiType.META_COPYRIGHT)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")	
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=Copyright()
+		self.text=text
+		return x
+			
+			
+			
+			
+			
+class TrackName(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_TRACK_NAME)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=TrackName()
+		self.text=text
+		return x
+		
+		
+		
+		
+		
+		
+class InstrumentName(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_INSTRUMENT_NAME)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=InstrumentName()
+		self.text=text
+		return x
+		
+		
+		
+		
+		
+		
+class TextLyrics(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_LYRICS)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=TextLyrics()
+		self.text=text
+		return x
+		
+		
+		
+		
+		
+		
+		
+class TextMarker(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_TEXT_MARKER)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=TextMarker()
+		self.text=text
+		return x
+		
+		
+		
+		
+		
+		
+		
+class CuePoint(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_CUE_POINT)
+		if fd:
+			self.text=fd.read(self.byte(fd)).decode("utf-8")
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 
+				len(self.text)]+[e.encode("hex") for e in self.text]
+		
+	def new(self, text):
+		x=CuePoint()
+		self.text=text
+		return x
+		
+		
+		
+		
+		
+		
+		
+		
+class ChannelPrefix(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_CHANNEL_PREFIX)
+		if fd:
+			self.byte(fd)
+			self.channelPrefix=self.byte(fd)
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 1, self.channelPrefix]
+		
+	def new(self, channelPrefix):
+		x=ChannelPrefix()
+		self.channelPrefix=channelPrefix
+		return x
+		
+		
+		
+		
+		
+		
+				
+class SetTempo(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_SET_TEMPO)
+		if fd:
+			self.byte(fd)
+			self.tempo=self.byte(fd)<<16
+			self.tempo+=self.byte(fd)<<8
+			self.tempo+=self.byte(fd)
+		
+	def bytes(self):
+		x=self.tempo
+		return [MidiType.META, self.command, 3, (x&0xFF0000)>>16,
+				(x&0xFF00)>>8, x&0xFF ]
+		
+	def new(self, tempo):
+		x=SetTempo()
+		self.tempo=tempo
+		return x
+		
+		
+		
+		
+class SMPTEOffset(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_SMPTE_OFFSET)
+		if fd:
+			self.byte(fd)
+			self.hour=self.byte(fd)
+			self.minutes=self.byte(fd)
+			self.seconds=self.byte(fd)
+			self.frame=self.byte(fd)
+			self.fraction=self.byte(fd)
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 5, self.hour, self.minutes,
+				self.seconds, self.frame, self.fraction]
+		
+	def new(self, hh, mm, ss, fr, ff):
+		x=SMPTEOffset()
+		x.hour=hh
+		x.minutes=mm
+		x.seconds=ss
+		x.frame=fr
+		x.fraction=ff
+		return x
+
+
+
+
+class TimeSignature(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_TIME_SIGNATURE)
+		if fd:
+			self.byte(fd)
+			self.numerator=self.byte(fd)
+			self.denominator=self.byte(fd)
+			self.clic=self.byte(fd)
+			self.notes=self.byte(fd)
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 4, self.numerator, self.denominator,
+				self.clic, self.notes]
+				
+	def new(self, numerator, denominator, clic, notes):
+		x=TimeSignature()
+		x.numerator=numerator
+		x.denominator=denominator
+		x.clic=clic
+		x.notes=notes
+		return x
+		
+class KeySignature(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_KEY_SIGNATURE)
+		if fd:
+			self.byte(fd)
+			self.sf=self.byte(fd)
+			self.mi=self.byte(fd)
+		
+	def bytes(self):
+		return [MidiType.META, self.command, 2, self.sf, self.mi]
+				
+	def new(self, sf, mi):
+		x=KeySignature()
+		self.sf=sf
+		self.mi=mi
+		return x
+		
+		
+class MetaSpecific(Meta):
+	def __init__(self, fd=None):
+		Meta.__init__(self, MidiType.META_SPECIFIC)
+		if fd:
+			self.length=self.byte(fd)
+			self.data=self.read(fd, self.length)
+		
+	def bytes(self):
+		return [MidiType.META, self.command, self.length, self.data]
+				
+	def new(self, data):
+		x=KeySignature()
+		self.data=data
+		self.length=len(data)
+		return x
+
+
+
+"""
+Error
+"""
+class MidiError(MidiMessage):
+	def __init__(self, first, offset):
+		MidiMessage.__init__(self, MidiType.ERROR)
+		raise MIDIParserException("First Midi byte ("+hex(first)+") is unknown at: "+hex(offset))
+
+	

+ 179 - 0
simplemidi/midiparser.py

@@ -0,0 +1,179 @@
+#!/usr/bin/python
+
+from .midimessage import *
+
+class MIDITrack:
+	def __init__(self):
+		pass
+
+class InputStreamWrapper:
+	def __init__(self, fd, mode="rb"):
+		if isinstance(fd, str):
+			fd=open(fd, mode)
+		self.fd=fd
+		self.buffer=[]
+		self.n=0
+		self.offset=0
+		self.doRecord=False
+		
+	def close(self):
+		self.fd.close()
+
+	
+	def read(self, n=-1):
+		x=self.fd.read(n)
+		self.offset+=n
+		if self.doRecord: self.buffer+=x
+		return x
+	
+	def record(self, rec=True):
+		self.doRecord=rec
+
+		
+
+class MIDIChunk:
+	def __init__(self, fd):
+		self.fd=fd
+		self.magick=""
+		self.length=0
+	
+	def parse(self):
+		pass
+	
+	def print(self):
+		pass
+	
+	def readMagick(self):
+		return self.fd.read(4).decode('ascii')
+	
+	def reverseMsb(self, x):
+		out=0
+		n=len(x)
+		for i in reversed(range(n)):
+			out+= (x[i]<< (8*(n-1-i)))
+		return out
+	
+	def intN(self, n):
+		return self.reverseMsb(self.fd.read(n))
+	
+	def int4(self):
+		return self.intN(4)
+		
+	def int2(self):
+		return self.intN(2)
+		
+	def byte(self):
+		return self.intN(1)
+		
+	def skip(self, n=1):
+		self.fd.read(n)
+
+class MIDIHeaderChunk(MIDIChunk):
+	def __init__(self, fd):
+		MIDIChunk.__init__(self,fd)
+		self.format=0
+		self.tracks=0
+		self.division=0
+		MIDIHeaderChunk.FORMAT_SINGLE_CHANNEL=0
+		MIDIHeaderChunk.FORMAT_MULTIPLE_CHANNEL=1
+		MIDIHeaderChunk.FORMAT_MULTIPLE_INDEPENDANT_CHANNEL=2
+		
+		self.variableTicks=False
+		self.ticksPerQuarter=0 # beat per quarter note
+	
+	def parse(self):
+		f=self.fd
+		self.magick=self.readMagick()
+		if self.magick!="MThd":
+			raise MIDIParserException("Bad header magick number")
+
+		self.length=self.int4()
+		self.format=self.int2()
+		self.tracks=self.int2()
+		self.division=self.int2()
+		
+		self.variableTicks=True if self.division & 0x8000 else False
+		if not self.variableTicks:
+			self.ticksPerQuarter= self.division & 0x7fff
+
+class MIDITrackChunk(MIDIChunk):
+	def __init__(self, fd, skipTrack=False):
+		MIDIChunk.__init__(self,fd)
+		self.time=0
+		self.events=[]
+		self.skipTrack=skipTrack
+	
+	def readDeltaTime(self):
+		out=[]
+		while True:
+			x=self.byte()
+			out.append(x&0x7F)
+			if not (x&0x80):
+				break
+		n=len(out)
+		ret=0
+		for i in range(n):
+			j=n-i-1
+			ret+= ( (out[i]) <<(j*7) )
+			
+		self.time+=ret
+		return ret
+	
+	def parse(self):
+		f=self.fd
+		self.magick=self.readMagick()
+		if self.magick!="MTrk":
+			raise MIDIParserException("Bad track magick number")
+		self.length=self.int4()
+		
+		events=[]
+		if self.skipTrack: self.fd.read(self.length)
+		else:
+			while True:
+				delta=self.readDeltaTime()
+				evt=self.readEvent()
+
+				
+				events.append((self.time, evt))
+				if evt.isEndTrack():
+					break
+		return events
+	
+
+			
+	def readEvent(self):
+		return MidiMessage.parse(self.fd)
+		
+class MidiFile:
+	def __init__(self, path):
+		self.path=path
+		self.tracksCount=0
+		self.header=None
+		self.tracks=[]
+		self.loaded=False
+	
+	def parse(self):
+		self.fd=0
+		try:
+			self.fd=open(self.path, "rb")
+		except Exception as e:
+			ERR(type(e).__name__+" : "+str(e))
+			return e
+		self.header=MIDIHeaderChunk(self.fd)
+		self.header.parse()
+		for t in range(self.header.tracks):
+			track=MIDITrackChunk(self.fd)
+			
+			self.tracks.append(track.parse())
+		self.fd.close()
+		self.loaded=True
+		
+	def isLoaded(self):
+		return self.loaded
+		
+
+if __name__=="__main__":	
+	#path="/home/fanch/Documents/tabs/Johann Pachelbel - Canon In D (ver 6 by Ezechiel).mid"
+	path="/home/fanch/Documents/tabs/Misc Traditional - Katyusha.mid"
+	m=MidiFile(path)
+	m.parse()

+ 211 - 0
simplemidi/midiplayer.py

@@ -0,0 +1,211 @@
+#!/usr/bin/python
+
+from .midiparser import MidiFile, MidiType
+from .midiio import MidiOutputPort
+from .options import*
+from .midieventtrigger import*
+import time
+
+def i2str(s, n):
+	prefix=""
+	nstr=str(s)
+	for i in range(n-len(nstr)):
+		prefix+=" "
+	return prefix+nstr
+
+
+def str2str(s, n):
+	prefix=""
+	for i in range(n-len(s)):
+		prefix+=" "
+	return prefix+s
+
+	
+class _Event:
+	def __init__(self, track, data):
+		t, evt=data
+		self.track=track
+		self.tick=t
+		self.type=evt.getType()
+		self.evt=evt
+		if self.type==MidiType.NOTE_OFF or self.type==MidiType.NOTE_ON or self.type==MidiType.KEY_PRESSURE or \
+		   self.type==MidiType.CONTROL_CHANGE or self.type==MidiType.PROGRAM_CHANGE or self.type==MidiType.CHANNEL_PRESSURE or\
+		   self.type==MidiType.PITCH_WHEEL_CHANGE:
+			   self.channel=evt.channel
+		else:
+			self.channel=-1
+			
+		if self.type==MidiType.NOTE_OFF or self.type==MidiType.NOTE_ON or self.type==MidiType.KEY_PRESSURE or \
+		   self.type==MidiType.CONTROL_CHANGE or self.type==MidiType.PROGRAM_CHANGE:
+			   self.key=evt.key
+		else:
+			self.key=-1
+	
+	def copy(self):
+		return _Event(self.track, (self.tick, self.evt.copy()))
+		
+	def hasChannel(self):
+		return self.channel>=0
+		
+	def hasKey(self):
+		return self.key>=0
+		
+	def transpose(self, n):
+		x=self.copy()
+		if self.hasKey() :
+			x.evt.transpose(n)
+			x.key=self.evt.key
+		return x
+	"""
+	Retourne un evenement transposé
+	"""
+	def __getitem__(self,key):
+		if isinstance(key, tuple):
+			return self.getButton(key[0], key[1])
+		else:
+			return self.getButton(key)
+	
+	def __lt__(self, e):
+		self.tick < e.tick
+	def __le__(self, e):
+		self.tick <= e.tick
+	def __lt__(self, e):
+		self.tick > e.tick
+	def __le__(self, e):
+		self.tick >= e.tick
+		
+	def __str__(self):
+		x=i2str(self.tick, 8)+" : "+str(self.track)+"  "+str2str(type(self.evt).__name__, 16)
+		if self.hasChannel(): x+="  "+i2str(self.channel,2)
+		if self.hasKey(): x+="  "+i2str(self.key,2)
+		return x;
+
+def sortTime(val): 
+    return val.tick  
+
+"""
+'port_in_params': {
+			'client_name' : 'MidiPlayer',
+			'port_name' : 'Pad In'
+		},
+		'port_in' : None,
+		'port_out_params': {
+			'client_name' : 'MidiPlayer',
+			'port_name' : 'Pad Out'
+		},
+		'port_out' : None,
+"""
+
+class MidiPlayer:
+	
+	_DEFAULT_PARAMS={
+		'port_sound_out': {
+			'client_name' : 'MidiPlayer',
+			'port_name' : 'Sound Out'
+		},
+		'transpose': 0,
+		'bpm' : 120,
+		'bpm_ratio': 1.0
+	}
+	
+	
+	def __init__(self, path, params):
+		if isinstance(path, str):
+			self.file=MidiFile(path)
+		else:
+			self.file=path
+			
+		self.file.parse()
+		self.header=self.file.header
+		self.tracks=self.file.tracks
+		self.nSoundTracks=self.header.tracks
+		self.nTotalTracks=len(self.file.tracks)
+		self.div=self.file.header.division
+		self.trackToPlay=list(range(self.nSoundTracks))
+		
+		param=initParams(MidiPlayer._DEFAULT_PARAMS, params)
+		self.port=MidiOutputPort.fromParams(param['port_sound_out'])
+		self.port.open()
+		
+		self.transposeNote=param['transpose']
+		self.bpm=param['bpm']
+		self.ratio=param['bpm_ratio']
+		
+		self.events=[]
+
+	def _load_track_event(self):
+		for track in range(len(self.tracks)):
+			for evt in self.tracks[track]:
+				self.events.append(_Event(track, evt))
+		self.events.sort(key=sortTime)
+
+	def tick2float(self, tick):
+		return tick*1.0/self.div
+	
+	def float2tick(self, f):
+		return f*self.div
+	
+	def tickBlanche(self): return int(self.float2tick(2))
+	def tickNoire(self): return int(self.float2tick(1))
+	def tickCroche(self): return int(self.float2tick(0.5))
+	def tickDoubleCroche(self): return int(self.float2tick(0.25))
+	
+	
+	def setBpmRatio(self, f):
+		self.ratio=f
+	
+	def tick2time(self, tick):
+		return tick*60.0/self.bpm/self.div/self.ratio
+
+	def transpose(self, n):
+		self.transposeNote+=n
+		#for e in self.events:
+		#	e.transpose(n)
+	
+	
+	def waitUpTo(self, tick):
+		if tick==0: return
+		t=self.tick2time(tick-self.tick)
+		time.sleep(t)
+		self.tick=tick
+	
+	def waitForEvent(self, evt):
+		tick=evt.tick
+		DEBUG("waiting "+str(tick-self.tick)+" ticks for: ",evt)
+		if tick<=0: return
+		t=self.tick2time(tick-self.tick)
+		time.sleep(t)
+		self.tick=tick
+		
+
+	def __initPlay(self):
+		self.tick=0
+		self.startTime=time.time()
+		self._load_track_event()
+		
+	
+	def send(self, evt) :
+		if not (evt.track in self.trackToPlay): return
+		if evt.type==MidiType.NOTE_ON:
+			self.port.send(evt.evt.bytes())
+		elif evt.type==MidiType.NOTE_OFF:
+			self.port.send(evt.evt)
+
+	def play(self):
+		self.__initPlay()
+		for evt in self.events:
+			evt=evt.transpose(self.transposeNote)
+			self.waitForEvent(evt)
+			self.send(evt)
+
+
+if __name__=="__main__":	
+	path="/home/fanch/Documents/tabs/Johann Pachelbel - Canon In D (ver 6 by Ezechiel).mid"
+	#path="/home/fanch/Documents/tabs/Misc Traditional - Katyusha.mid"
+	m=MidiPlayer(path)
+	m.setBpmRatio(0.1)
+	#m.transpose(24)
+	m.open("Player")
+	input("Press Enter to continue...")
+	#m.connect((28,0))
+	m.play()

+ 92 - 0
simplemidi/options.py

@@ -0,0 +1,92 @@
+#!/usr/bin/python
+import time
+import copy
+OPT_DEBUG_LEVEL=0
+
+def LOG_LVL_TO_STRING(n):
+	return ["MIDIDEBUG", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"][n]
+
+def SET_LOG_LEVEL(n):
+	getOptions().log.setLogLevel(n)
+
+import collections
+
+def dicUpdate(d, u):
+	for k, v in u.items():
+		if isinstance(v, collections.Mapping):
+			d[k] = dicUpdate(d.get(k, {}), v)
+		else:
+			d[k] = v
+	return d
+
+def dictAssign(a, *mods):
+	a=copy.deepcopy(a)
+	for i in mods:
+		dicUpdate(a, i)
+	return a
+
+def initParams( default, *params):
+	if params==None:
+		raise Exception("Parametre non passés")
+	return dictAssign(default, *params)
+		
+
+
+
+class Log:
+	def __init__(self, lvl=1):
+		self.level=lvl
+		Log.CRITICAL=5
+		Log.ERROR=4
+		Log.WARNING=3
+		Log.INFO=2
+		Log.DEBUG=1
+		Log.MIDIDEBUG=0
+		
+	
+	def setLogLevel(self, lvl): 
+		self.level=lvl
+	
+	def log(self, level, *args):
+		if self.level>level: return
+		x=""
+		for i in args:
+			x+=str(i)
+		for i in x.split('\n'):
+			print("%.3f" %TIME(),":",LOG_LVL_TO_STRING(level),":",i)
+		
+	
+	def midiDebug(self, *args): self.log(Log.MIDIDEBUG, *args)
+	def debug(self, *args): self.log(Log.DEBUG, *args)
+	def info(self, *args): self.log(Log.INFO, *args)
+	def warn(self, *args): self.log(Log.WARNING, *args)
+	def err(self, *args): self.log(Log.ERROR, *args)
+	def crit(self, *args): self.log(Log.CRITICAL, *args)
+
+
+def CRIT(*args):	getOptions().log.crit(*args)
+def ERR(*args):	getOptions().log.err(*args)
+def WARN(*args):	getOptions().log.warn(*args)
+def INFO(*args):	getOptions().log.info(*args)
+def DEBUG(*args):	getOptions().log.debug(*args)
+def MIDIDEBUG(*args):	getOptions().log.midiDebug(*args)
+
+def TIME(): return time.time()-getOptions().startTime
+
+class SimpleMidiOptions:
+	def __init__(self):
+		self.startTime=time.time()
+		self.log=Log()
+
+def _initOption():
+	global ___SIMPLE_MIDI_OPTION_OBJ__  # add this line!
+	try: 
+		x=___SIMPLE_MIDI_OPTION_OBJ__
+	except:	
+		___SIMPLE_MIDI_OPTION_OBJ__ = SimpleMidiOptions()
+	
+_initOption()
+
+def getOptions():
+	global ___SIMPLE_MIDI_OPTION_OBJ__
+	return ___SIMPLE_MIDI_OPTION_OBJ__

+ 69 - 0
simplemidi/utils.py

@@ -0,0 +1,69 @@
+#!/usr/bin/python
+
+from threading import Thread, Lock
+import time
+
+def hexstr(b):
+	x="["
+	for i in range(len(b)):
+		if i>0: x+=", "
+		x+=hex(b[i])
+	return x+"]"
+
+
+class Queue:
+	def __init__(self, size=1024):
+		self.head=0
+		self.tail=0
+		self.alloc=size
+		self.data=[]
+		self.mutex=Lock()
+		for i in range(self.alloc): self.data.append(None)
+	
+	def enqueue(self, x):
+		self.mutex.acquire()
+		self.data[self.tail]=x
+		self.tail= (self.tail+1)%self.alloc
+		self.mutex.release()
+		
+		
+	def dequeue(self):
+		while self.isEmpty(): time.sleep(0.001)
+		self.mutex.acquire()
+		x=self.data[self.head]
+		self.head=(self.head+1)%self.alloc
+		self.mutex.release()
+		return x
+	
+	def __length(self):
+		if self.head>self.tail:
+			return self.alloc-(self.head-self.tail)
+		return self.tail-self.head
+	
+	def length(self):
+		self.mutex.acquire()
+		x= self.__length()
+		self.mutex.release()
+		return x
+		
+	def isEmpty(self):
+		return  self.length()==0
+			
+	def peek(self):
+		self.mutex.acquire()
+		x=self.data[self.head]
+		self.mutex.release()
+		return x
+		
+	def contains(self, x):
+		n=self.length()
+		ok=False
+		self.mutex.acquire()
+		for i in range(self.head, self.head+n):
+			j=i%self.alloc
+			if self.data[j] == x:
+				ok=True
+				break
+		self.mutex.release()
+		return ok
+		

+ 104 - 0
syncpadhelper.py

@@ -0,0 +1,104 @@
+#!/usr/bin/python
+
+from padhelper import PadHelper, _Event
+from simplemidi.options import *
+from simplemidi.midimessage import *
+from simplemidi.utils import *
+
+from threading import Thread, Lock
+import time
+
+
+class SyncEvent(_Event):
+	def __init__(self, evt):
+		_Event.__init__(self, 0, (evt.tick, evt.evt))
+		self.startTime=time.time()
+		
+	def __eq__(self, x):
+		return x.tick==evt.tick and x.evt==self.evt
+
+class SyncPadHelper(PadHelper):
+	
+	_DEFAULT_PARAMS=dictAssign(PadHelper._DEFAULT_PARAMS, {
+		'forward_all_input': True
+	})
+	
+	def __init__(self, adapter, path, params):
+		param=initParams(SyncPadHelper._DEFAULT_PARAMS, params)
+		PadHelper.__init__(self, adapter, path, params)
+		self.lockQueue=Queue()
+		self.forwardAllKey=param['forward_all_input']
+		
+	
+	"""
+	def send(self, evt) :
+		if evt.track==self.padTrack :
+			if evt.type==MidiType.NOTE_ON:
+				if (evt.key+self.padTranslate) in self.ledbuttons: 
+					self[evt.key+self.padTranslate]=evt.evt.velocity
+					if evt.evt.velocity==1:
+						self.lockQueue
+		else:
+			MidiPlayer.send(self, evt)
+	"""
+	
+	def _findNoteEnd(self, istart, evt):
+		for i in range(istart+1, len(self.events)):
+			e=self.events[i]
+			if e.type==MidiType.NOTE_ON and e.key==evt.key and e.evt.velocity==0:
+				return e.transpose(self.transposeNote)
+		return None
+	
+	def inputMessage(self, delta, msg):
+		channel=msg.channel
+		if msg.getType()==MidiType.NOTE_ON:
+			x=self.lockQueue.peek()
+			msg.key-=self.padTranslate
+			if x!=None:
+				if x.channel==msg.channel and x.key+self.transposeNote==(msg.key):
+					self.lockQueue.dequeue()
+					self.port.send(msg)
+					
+				elif self.forwardAllKey: self.port.send(msg)
+				
+			elif self.forwardAllKey: self.port.send(msg)
+			
+		elif msg.getType()==MidiType.NOTE_OFF:
+			msg.key-=self.padTranslate
+			self.port.send(msg)
+			
+		elif self.forwardAllKey:
+			self.port.send(msg)
+				
+	
+	def waitForEvent(self, evt):
+		tick=evt.tick
+		#DEBUG("waiting "+str(tick-self.tick)+" ticks for: ",evt)
+		if tick<=0: return
+		while not self.lockQueue.isEmpty():
+			time.sleep(0.001)
+
+		t=self.tick2time(tick-self.tick)
+		time.sleep(t)
+		self.tick=tick
+		
+			
+	def play(self):
+		self._load_track_event()
+		self.tick=0
+		self.startTime=time.time()
+		for i in range(len(self.events)):
+			evt=self.events[i].transpose(self.transposeNote)
+			self.waitForEvent(evt)
+			
+			#self.addToQueue(i,3)
+			
+			
+			if evt.track==self.padTrack and evt.type==MidiType.NOTE_ON:
+				self[evt.key+self.padTranslate+self.transposeNote]=evt.evt.velocity
+				if evt.evt.velocity==1:
+					end=self._findNoteEnd(i, self.events[i])
+					self.lockQueue.enqueue(evt)
+			else:	
+				self.send(evt)
+			

+ 280 - 0
tabextract.py

@@ -0,0 +1,280 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from lxml import etree
+import sys
+import getopt
+
+NOTE_TYPE=     ["whole", "half", "quarter", "eighth", "16th", "32nd", "64th"]
+NOTE_TYPE_MULT=[      4,      2,         1,      0.5,   0.25,  0.125, 0.0625]
+
+def noteTypeToIndex(s):
+	i=0
+	for st in NOTE_TYPE:
+		if s==st:
+			return i
+		i=i+1
+	return -1
+
+def fretToMidi(string, fret):
+	tab=[0,40,45,50,55,59,64]
+	tab=[0,64,59,55,50,45,40]
+	return tab[string]+fret
+
+def xpathExists(obj, path):
+	for x in obj.xpath(path):
+		return True
+	return False
+	
+def xpathFirst(d, path):
+	for x in d.xpath(path):
+		return x
+	return None
+
+class Note:
+	def __init__(self, data):
+		self.octave=0
+		self.note="R"
+		self.corde=0
+		self.fret=0
+		self.midiNote=0
+		if xpathExists(data,"rest"):
+			self.rest=True
+		else:
+			self.rest=False
+			self.octave=int(xpathFirst(data,"pitch/octave").text)
+			self.note=xpathFirst(data,"pitch/step").text
+			self.corde=int(xpathFirst(data,"notations/technical/string").text)
+			self.fret=int(xpathFirst(data,"notations/technical/fret").text)
+			self.midiNote=fretToMidi(self.corde, self.fret)
+		
+		self.type=noteTypeToIndex(xpathFirst(data,"type").text)
+		self.duree=int(xpathFirst(data,"duration").text)
+		self.voix=int(xpathFirst(data,"voice").text)
+		self.dot=xpathExists(data, "dot")
+		self.beatmult=NOTE_TYPE_MULT[self.type]
+		if self.dot:
+			self.beatmult=1.5*self.beatmult
+	
+		
+	def getMidi(self):
+		return self.midiNote
+	
+	def getTime(self):
+		return self.beatmult
+
+	def show(self):
+		print(str(self.getMidi())+": \""+str(self.type)+"\" "+str(self.dot)+" "+str(self.rest))
+
+class Measure:
+	def __init__(self, data):
+		self.notes=[]
+		try:
+			self.beats=int(xpathFirst(data, "attributes/time/beats").text)
+			self.beatType=int(xpathFirst(data, "attributes/time/beat-type").text)
+		except:
+			self.notes=[]
+		
+	def addNote(self, note):
+		self.notes.append(note)
+		
+	def noteToString(self):
+		s=""
+		i=0
+		for x in self.notes:
+			if i>0:
+				s+=", "
+			s+=str(self.notes[i].getTime())
+			i+=1
+		return s
+	
+	def timeToString(self):
+		s=""
+		i=0
+		for x in self.notes:
+			if i>0:
+				s+=", "
+			s+=str(self.notes[i].getTime())
+			i+=1
+		return s
+		
+class Part:
+	def __init__(self, data):
+		self.id=data.get("id")
+		self.notes=[]
+		self.beat=0
+		self.beatType=0
+		path="/score-partwise/part-list/score-part[@id='"+self.id+"']/"
+		self.name=xpathFirst(data, path+"part-name").text
+		self.instrument=xpathFirst(data, path+"score-instrument/instrument-name").text
+		self.midiChannel=xpathFirst(data, path+"midi-instrument/midi-channel").text
+		self.midiProgram=xpathFirst(data, path+"midi-instrument/midi-program").text
+		self.measures=[]
+
+		for d in data.xpath("measure"):
+			m=Measure(d)
+			self.measures.append(m)
+			for n in d.xpath("note"):
+				note=Note(n)
+				self.notes.append(note)
+				m.addNote(note)
+				
+	def length(self):
+		tot=0
+		for x in self.notes:
+			tot+=x.getTime()
+		return tot
+	
+	def toString(self):
+		return self.name+" : (\""+self.instrument+"\", "+str(self.length())+" beats, "+str(len(self.notes))+" notes)"
+
+	def noteRangeToString(self, start, end):
+		i=start
+		s=""
+		while i<=end:
+			for x in self.measures[i].notes:
+				s+= ("" if s=="" else ", ")+str(x.getMidi())
+			i+=1
+		return s
+		
+	def timeRangeToString(self, start, end):
+		i=start
+		s=""
+		while i<=end:
+			for x in self.measures[i].notes:
+				s+= ("" if s=="" else ", ")+str(x.getTime())
+			i+=1
+		return s
+		
+		
+class  System:
+	def __init__(self, path):
+		self.path=path
+		self.partition=[]
+		self.tree = etree.parse(self.path)
+		self.parts=[]
+		for data in self.tree.xpath("/score-partwise/part"):
+			self.parts.append(Part(data))
+			
+		self.bufferNote=""
+		self.bufferTime=""
+	
+		
+	def noteRange(self, i, start, end):
+		x=self.parts[i-1].noteRangeToString(start-1, end-1)
+		self.bufferNote+= ("" if self.bufferNote=="" else ", ")+x 
+		return x;
+		
+	def timeRange(self, i, start, end):
+		x=self.parts[i-1].timeRangeToString(start-1, end-1)
+		self.bufferTime+= ("" if self.bufferTime=="" else ", ")+x 
+		return x;
+		
+	def note(self, i):
+		return self.noteRange(i, 1, len(self.parts[i-1].measures));
+		
+	def time(self, i):
+		return self.timeRange(i, 1, len(self.parts[i-1].measures));
+		
+	def range(self, i,start, end):
+		self.timeRange(i, start, end)
+		self.noteRange(i, start, end)
+	
+	def track(self, i):
+		self.time(i)
+		self.note(i)
+		
+	def before(self, i, n):
+		self.timeRange(i, 1, n)
+		self.noteRange(i, 1, n)
+		
+	def after(self, i, n):
+		self.timeRange(i, n, len(self.parts[i-1].measures))
+		self.noteRange(i, n, len(self.parts[i-1].measures))
+		
+	def new(self):
+		self.bufferNote=""
+		self.bufferTime=""
+		
+	def write(self, f):
+		 fd = open(f, "w")
+		 fd.write("notes=(ring "+self.bufferNote+")")
+		 fd.write("times=(ring "+self.bufferTime+")")
+		 fd.close()
+		 
+	
+	def print(self):
+		print("notes=(ring "+self.bufferNote+")")
+		print("times=(ring "+self.bufferTime+")")
+	
+	def list(self):
+		i=0
+		for x in self.parts:
+			print("["+str(i)+"] : "+x.toString())
+			i+=1
+if __name__ == '__main__':
+	def usage():
+		print("Usage:")
+		print(sys.argv[0]+"  PATH_TO_XML [COMMANDS]")
+		print("ou " +sys.argv[0]+"  -h | --help : Affiche cette aide")
+		print("ou " +sys.argv[0]+"  -l | --list : Liste les pistes")
+		print("COMMANDS:")
+		print("\t-n | --new : Vide le buffer courant")
+		print("\t-r | --range TRACK START END : Ajoute les mesure de la piste TRACK entre les mesure START et END")
+		print("\t-b | --before TRACK INDEX : Ajoute les mesure de la piste TRACK jusqu'a la mesure INDEX")
+		print("\t-a | --after TRACK INDEX : Ajoute les mesure de la piste TRACK à partir de la mesure INDEX")
+		print("\t-t | --track TRACK : Ajoute toutes la piste TRACK au buffer")
+		print("\t-p | --print : Affiche le buffer courant")
+		print("\t-w | --write FILE : Ecrit le buffer dans le fichier FILE")
+
+
+
+	song=None
+	path="/home/fanch/Documents/tabs/Johann Pachelbel - Canon In D (ver 6 by Ezechiel).xml"
+	i=1
+	op=False
+	if len(sys.argv)==1:
+		usage()
+		sys.exit(-1)
+	while i<len(sys.argv):
+		if i==1:
+			path=sys.argv[i]
+			i+=1
+			song=System(path)
+		elif sys.argv[i]=="-n" or sys.argv[i]=="--new":
+			song.new()
+		elif sys.argv[i]=="-r" or sys.argv[i]=="--range":
+			song.range(int(sys.argv[i+1]), int(sys.argv[i+2], sys.argv[i+3]))
+			op=True
+			i+=3
+		elif sys.argv[i]=="-b" or sys.argv[i]=="--before":
+			song.before(int(sys.argv[i+1]), int(sys.argv[i+2]))
+			op=True
+			i+=2
+		elif sys.argv[i]=="-a" or sys.argv[i]=="--after":
+			song.after(int(sys.argv[i+1]), int(sys.argv[i+2]))
+			op=True
+			i+=2
+		elif sys.argv[i]=="-t" or sys.argv[i]=="--track":
+			op=True
+			song.track(int(sys.argv[i+1]))
+			i+=1
+		elif sys.argv[i]=="-p" or sys.argv[i]=="--print":
+			song.print()
+		elif sys.argv[i]=="-w" or sys.argv[i]=="--write":
+			song.part(int(sys.argv[i+1]))
+			i+=1
+		elif sys.argv[i]=="-h" or sys.argv[i]=="--help":
+			usage()
+			sys.exit(-1)
+		elif sys.argv[i]=="-l" or sys.argv[i]=="--list":
+			usage()
+			sys.exit(-1)
+		else:
+			usage()
+			sys.exit(-1)
+		i+=1
+	if not op:
+		song.track(1)
+	song.print()
+