#!/usr/local/bin/python # # Midi/In.py -- a higher-level interface to MIDI._In # # Copyright (C) 1999-2000 Eric S. Tiedemann # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Library General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Library General Public License for more details. # # You should have received a copy of the GNU Library General Public # License along with this library; if not, write to the Free # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Contact: Eric S. Tiedemann, est@hyperreal.org # _version = "0.9.0" _types = __import__('types') _midi = __import__('MIDI._In') # set for C callback dispatch..otherwise dispatch is done in python _Ccb = 1 class MidiIn: """MIDI input stream parser.""" # range of values various MIDI data can assume __token_ranges = ( ((1,16), 'channel'), ((0,16), 'nchannels'), ((0,1), 'bool'), ((0,119), 'control'), ((0, 2**14 - 1), 'bend', 'position'), # 14-bit values ((0, 127), 'key', 'velocity', 'value', 'program', 'pressure', 'mtcval', 'song'), ) # create a map from token names to value ranges __tokens = {} for __tup in __token_ranges: __r = __tup[0] __ts = __tup[1:] for __t in __ts: __tokens[__t] = __r # grammar of MIDI messages received from MIDI._In __grammar = { # channel voice messages 'note-off':('channel', 'key', 'velocity',), 'note-on':('channel', 'key', 'velocity',), 'poly-key-pressure':('channel', 'key', 'velocity',), 'control-change':('channel', 'control', 'value',), 'program-change':('channel', 'program',), 'channel-pressure':('channel', 'pressure',), # the two bend bytes are combined in one 14-bit value 'pitch-bend':('channel', 'bend',), # channel mode messages 'all-sound-off':('channel',), 'reset-all-controllers':('channel',), # bool is 0 or 1 'local-control':('channel', 'bool',), 'all-notes-off':('channel',), 'omni-off':('channel',), 'omni-on':('channel',), # nchannels is number of channels or 0 for number of voices in receiver 'mono-on':('channel', 'nchannels',), 'poly-on':('channel',), # system common messages 'MTC-quarter-frame':('mtcval',), # the two position bytes are combined in one 14-bit value 'song-position-pointer':('position',), 'song-select':('song',), 'tune-request':(), # system real-time messages 'timing-clock':(), 'start':(), 'continue':(), 'stop':(), 'active-sensing':(), 'system-reset':(), } # the MIDI callback structure is arranged in a table with a # top-level dictionary and nested lists. all this is constructed # lazily as various slots are assigned. the callback dispatcher # (self.cb) knows how to traverse this lazy structure. the # structure is modified via calls to __setitem__ below. def __setitem__(self, i, v): #print "%s.__setitem__(%s, %s)" % (self, i, v) if v != None and not callable(v): raise TypeError, "value is not callable or None" # normalize indices to tuple if type(i) != _types.TupleType: i = (i,) if i[0] == Ellipsis: # an initial ellipsis can be used to register one function # for *all* message types if len(i) != 1: raise ValueError, "too many terms in subscript" else: self.__tab = v else: msg = i[0] if not type(msg) == _types.StringType: raise TypeError, "the first arg must be a string" if not self.__grammar.has_key(msg): raise ValueError, "%s is not a valid message name" % msg if not self.__tab or type(self.__tab) != _types.DictType: # ok..we need to construct that top-level dict # handing __zap() the current scalar value for __tab. self.__tab = {msg:self.__zap(msg, self.__tab, i[1:], v, self.__grammar[msg])} else: # top-level dict already exists, so __zap() should be # called using whatever's already in the MSG slot self.__tab[msg] = self.__zap(msg, self.__tab.get(msg), i[1:], v, self.__grammar[msg]) if _Ccb: self.__m.settab(self.__tab) # the scarier function for modifying the callback table :) # # it's a recursive function for chewing down remaining setitem # indices (IDXS) and grammar terms (GRAM) that returns a new value # for the table based on the current one for this level passed in # (TAB) and the callback value V. it's recursive nature means the # if their are any exceptions, no modifications are done. MSG is # the message type and is used for error messages. # # each index can be a number, a simple numeric slice, or an ellipsis def __zap(self, msg, tab, idxs, v, gram): #print 'zap', msg, tab, idxs, v, gram if not gram and idxs: raise ValueError, "too many indices for %s" % msg elif not idxs: return v i = idxs[0] tok = gram[0] r = self.__tokens[tok] if i == Ellipsis: if type(tab) != _types.ListType: tab = [tab] * (r[1] + 1) for i in range(len(tab)): tab[i] = self.__zap(msg, tab[i], idxs[1:], v, gram[1:]) return tab elif type(i) == _types.IntType: #print i, r if i < r[0] or i > r[1]: raise ValueError, "index out of range for %s" % gram[0] if type(tab) != _types.ListType: tab = [tab] * (r[1] + 1) #print tab tab[i] = self.__zap(msg, tab[i], idxs[1:], v, gram[1:]) #print tab return tab elif type(i) == _types.SliceType: if i.step: raise TypeError, "stepping ranges aren't allowed as indices" start, stop = i.start, i.stop if not start: start = r[0] if not stop: stop = r[1] if type(start) != _types.IntType or type(stop) != _types.IntType: raise TypeError, "ranges must be of integers" if start < r[0] or stop > r[1]: raise ValueError, "invalid range for %s" % gram[0] if type(tab) != _types.ListType: tab = [tab] * (r[1] + 1) for i in range(start, stop+1): tab[i] = self.__zap(msg, tab[i], idxs[1:], v, gram[1:]) return tab else: raise TypeError, "invalid index %s" % i # the callback dispatcher. MSG and ARGS come direct from MIDI._In. # this isn't used if _Ccb is set. def __cb(self, msg, *args): #print msg, args if not self.__tab: return if type(self.__tab) != _types.DictType: apply(self.__tab, (msg,) + args) return cb = self.__tab.get(msg) args0 = args # tear down the callback table structure based on number of # args received. while cb and type(cb) == _types.ListType: if not args: raise RuntimeError, \ "insufficient arguments for message type %s" % msg cb = cb[args[0]] args = args[1:] if cb: apply(cb, (msg,) + args0) def __init__(self, sysex_cb = None): self.__tab = None if _Ccb: if sysex_cb: self.__m = _midi._In.new1(self.__tab, sysex_cb) else: self.__m = _midi._In.new1(self.__cb) else: if sysex_cb: self.__m = _midi._In.MidiIn(self.__cb, sysex_cb) else: self.__m = _midi._In.MidiIn(self.__cb) def inbytes(self, bs): self.__m.inbytes(bs) def read(self, f): self.__m.read(f) if (__name__ == '__main__'): def testloop(): import os def scb(s): print 'system exclusive:', map(ord, s) m = MidiIn(scb) def foo(msg, *args): print msg, args m[...] = foo while 1: bs = os.read(0, 1000) m.inbytes(bs) import sys if len(sys.argv) > 1: import profile p = profile.run('testloop()', 'midistats') import pstats p = pstats.Stats('midistats') p.sort_stats('cumulative').print_stats(20) p.sort_stats('time').print_stats(20) else: testloop()