1- from collections import defaultdict
1+ import asyncio
22
33import mido
44
5+ # not sure why this didn't work in MIDI class.
6+ async def midi_coroutine (self ):
7+ while True :
8+ for port_name , port in self .in_ports .items ():
9+ # print(port_name, port)
10+ for m in port .iter_pending ():
11+ # print(port_name, m)
12+ for filters , f in self .handlers :
13+ use_handler = (
14+ 'port' not in filters or port_name in filters .pop ('port' ))
15+ use_handler &= all (
16+ filt is None
17+ or not hasattr (m , k )
18+ or getattr (m , k ) in filt
19+ for k ,filt in filters .items ())
20+ if use_handler : f (m )
21+ # print([(
22+ # filt is None,
23+ # not hasattr(m, k),
24+ # getattr(m, k) in filt, k, filt)
25+ # for k,filt in filters.items()])
26+
27+ await asyncio .sleep (self .sleep_time )
28+
529def _get_filter (item ):
630 if item is None :
731 return item
8- if hasattr (item , '__iter__' ):
32+ if ( not isinstance ( item , str )) and hasattr (item , '__iter__' ):
933 return set (item )
1034 return {item }
1135
1236class MIDI :
1337 """"""
14- def __init__ (self , in_ports = None , out_ports = None , verbose = True ):
38+ instances = []
39+ def __init__ (self , in_ports = None , out_ports = None , verbose = True , sleep_time = 0.0005 ):
1540 """
1641 Args:
1742 in_ports: list of input devices (uses all by default)
1843 out_ports: list of output devices (uses all by default)
1944 """
2045 self .verbose = verbose
46+ self .sleep_time = sleep_time
2147 # type -> list[Optional[set[port], Optional[set[channel]], function]
22- self .handlers = defaultdict ( list )
48+ self .handlers = []
2349
2450 if in_ports is None or len (in_ports )== 0 :
2551 in_ports = mido .get_input_names ()
2652 self .in_ports = {# mido.ports.MultiPort([
27- port : mido .open_input (port , callback = self .get_midi_callback ())
53+ port : mido .open_input (port ) # , callback=self.get_midi_callback())
2854 for port in in_ports
2955 }
3056
@@ -41,107 +67,54 @@ def __init__(self, in_ports=None, out_ports=None, verbose=True):
4167 if self .verbose :
4268 print (f"""opened MIDI output ports: { list (self .out_ports )} """ )
4369
44- self .handle = MIDIHandlers (self )
45-
46- # TODO: filtering needs to work for all message types...
47- # maybe there should be just one decorator
48- # which can accept any filter, including on type?
49- # and which just takes a mido message?
50-
51- def get_midi_callback (self ):
52- """callback for mido MIDI handling"""
53- # close over `self``
54- def callback (m ):
55- """dispatch to decorated midi handlers"""
56- # logging.debug(m)
57- handlers = self .handlers [m .type ]
58- for ports , channels , numbers , values , f in handlers :
59- use_handler = (
60- (ports is None or m .port in ports ) and
61- (channels is None or m .channel in channels ) and
62- (numbers is None or m .number in numbers ) and
63- (values is None or m .value in values )
64- )
65- if use_handler :
66- f (m )
67- return callback
68-
69- def _decorator (self , msg_type ,
70- ports = None , channels = None , numbers = None , values = None ):
71- """generic MIDI handler decorator"""
72- if hasattr (ports , '__call__' ):
70+ # self.handle = MIDIHandlers(self)
71+ MIDI .instances .append (self )
72+
73+ def handle (self , * a , ** kw ):
74+ """MIDI handler decorator"""
75+ if len (a ):
7376 # bare decorator
74- f = ports
75- assert channels is None
76- assert numbers is None
77- assert values is None
77+ assert len (a )== 1
78+ assert len (kw )== 0
79+ assert hasattr (a [0 ], '__call__' )
80+ f = a [0 ]
81+ filters = {}
7882 else :
7983 # with filter arguments
84+ for k in kw :
85+ assert k in {
86+ 'channel' , 'port' , 'type' ,
87+ 'note' , 'velocity' , 'value' ,
88+ 'control' , 'program'
89+ }, f'unknown MIDI message filter "{ k } "'
90+ filters = {k :_get_filter (v ) for k ,v in kw .items ()}
8091 f = None
81- ports = _get_filter (ports )
82- channels = _get_filter (channels )
83- numbers = _get_filter (numbers )
84- values = _get_filter (values )
8592
8693 def decorator (f ):
87- self .handlers [msg_type ].append ([
88- ports , channels , numbers , values , f
89- ])
94+ self .handlers .append ((filters , f ))
9095 return f
9196
9297 return decorator if f is None else decorator (f )
9398
94- # send on all ports by default
95- def _send ( self , port , * a , ** kw ):
99+ def _send_msg ( self , port , m ):
100+ """send on a specific port or all output ports"""
96101 ports = self .out_ports .values () if port is None else [self .out_ports [port ]]
97102 for p in ports :
98- p .send (mido .Message (* a , ** kw ))
99-
100- # see https://mido.readthedocs.io/en/latest/message_types.html
101-
102- def note_on (self , note , velocity = 64 , channel = 0 , port = None ):
103- self ._send (port , 'note_on' , channel = channel , note = note , velocity = velocity )
104-
105- def note_off (self , note , velocity = 64 , channel = 0 , port = None ):
106- self ._send (port , 'note_off' , channel = channel , note = note , velocity = velocity )
107-
108- def control_change (self , control , value , channel = 0 , port = None ):
109- self ._send (port , 'control_change' , channel = channel , control = control , value = value )
110-
111- def cc (self , * a , ** kw ):
112- self .control_change (* a , ** kw )
113-
114- def program_change (self , program , channel = 0 , port = None ):
115- self ._send (port , 'program_change' , channel = channel , program = program )
116-
117- def polytouch (self , note , value = 64 , channel = 0 , port = None ):
118- self ._send (port , 'polytouch' , channel = channel , note = note , value = value )
119-
120- def aftertouch (self , value = 64 , channel = 0 , port = None ):
121- self ._send (port , 'aftertouch' , channel = channel , value = value )
122-
123- def pitchwheel (self , pitch = 0 , port = None ):
124- self ._send (port , 'pitchwheel' , pitch = pitch )
125-
126- def pitchbend (self , * a , ** kw ):
127- self .pitchwheel (* a , ** kw )
128-
129-
130- # this is effectively part of MIDI class,
131- # it is only a separate class for naming aesthetics
132- class MIDIHandlers :
133- """specific MIDI handler decorators"""
134- def __init__ (self , midi ):
135- self .midi = midi
136-
137- def note_on (self , ports = None , channels = None , notes = None , velocities = None ):
138- return self .midi ._decorator ('note_on' , ports , channels , notes , velocities )
139-
140- def note_off (self , ports = None , channels = None , notes = None , velocities = None ):
141- return self .midi ._decorator ('note_off' , ports , channels , notes , velocities )
142-
143- def control_change (self , ports = None , channels = None , controls = None , values = None ):
144- return self .midi ._decorator ('control_change' , ports , channels , controls , values )
103+ p .send (m )
104+
105+ # # see https://mido.readthedocs.io/en/latest/message_types.html
106+
107+ def send (self , m , * a , port = None , ** kw ):
108+ """send a mido message"""
109+ if isinstance (m , mido .Message ):
110+ self ._send_msg (port , m )
111+ if len (a )+ len (kw ) > 0 :
112+ print ('warning: extra arguments to MIDI send' )
113+ elif isinstance (m , str ):
114+ try :
115+ self ._send_msg (port , mido .Message (m , * a , ** kw ))
116+ except Exception :
117+ print ('MIDI send failed: bad arguments to mido.Message' )
118+ else :
119+ print ('MIDI send failed: first argument should be a mido.Message or str' )
145120
146- def cc (self , * a , ** kw ):
147- return self .control_change (* a , ** kw )
0 commit comments