Skip to content
This repository was archived by the owner on Nov 23, 2023. It is now read-only.

Commit 789b3b6

Browse files
MIDI support
1 parent 7548dd5 commit 789b3b6

3 files changed

Lines changed: 85 additions & 105 deletions

File tree

examples/iipyper/iipyper-tutorial.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import mido
2+
13
from iipyper import OSC, MIDI, repeat, run
24

35
# TODO: MIDI
@@ -19,15 +21,16 @@ def _():
1921
# # midi.note_on, midi.cc, etc
2022

2123
# # decorator to make a midi handler:
22-
# # here filtering for pitch > 0, channel = 0
23-
# @midi.handle.note_on(notes=range(1,128), channels=0)
24-
# def _(note, velocity, channel):
25-
# print(note, velocity, channel)
24+
# # here filtering for type='note_on', note > 0, channel = 0
25+
@midi.handle(type='note_on', note=range(1,128), channel=0)
26+
def _(msg):
27+
print(msg)
2628

2729
# function to send MIDI:
28-
midi.note_on(note=60, velocity=100, channel=0)
29-
midi.note_off(note=60, velocity=100, channel=0)
30-
midi.cc(control=0, value=127, channel=1)
30+
midi.send('note_on', note=60, velocity=100, channel=0)
31+
# or mido-style:
32+
m = mido.Message('note_off', note=60, velocity=100, channel=0)
33+
midi.send(m)
3134

3235
# @repeat(1)
3336
# def _():

iipyper/iipyper/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ async def _run_async():
2626
for osc in OSC.instances:
2727
await osc.create_server(asyncio.get_event_loop())
2828
# osc.create_client()
29-
29+
30+
for midi in MIDI.instances:
31+
asyncio.create_task(midi_coroutine(midi))
32+
# asyncio.create_task(midi.get_coroutine())
33+
3034
# start loop tasks
3135
if len(_loop_fns):
3236
for f in _loop_fns:
3337
asyncio.create_task(f())
34-
# else:
38+
3539
while True:
3640
await asyncio.sleep(1)
3741

iipyper/iipyper/midi.py

Lines changed: 69 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,56 @@
1-
from collections import defaultdict
1+
import asyncio
22

33
import 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+
529
def _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

1236
class 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

Comments
 (0)