Skip to content

Commit f133bfc

Browse files
committed
Input aftertouch/polytouch support
1 parent 6240b6a commit f133bfc

File tree

2 files changed

+125
-8
lines changed

2 files changed

+125
-8
lines changed

isobar/io/midi/input.py

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import mido
2-
31
import os
2+
import mido
43
import time
54
import queue
65
import logging
7-
from typing import Any
6+
from typing import Any, Callable
87
from ...exceptions import DeviceNotFoundException
98
from ...constants import MIDI_CLOCK_TICKS_PER_BEAT
9+
from ..midinote import MidiNote
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -31,22 +31,35 @@ def __init__(self,
3131
if device_name is None:
3232
device_name = os.getenv("ISOBAR_DEFAULT_MIDI_IN")
3333
try:
34-
self.midi = mido.open_input(device_name, callback=self._callback, virtual=virtual)
34+
self.midi = mido.open_input(name=device_name,
35+
callback=self._mido_callback,
36+
virtual=virtual)
3537
except (RuntimeError, SystemError, OSError):
3638
raise DeviceNotFoundException("Could not find MIDI device")
3739

3840
self.clock_target = clock_target
3941
self.queue = queue.Queue()
40-
self.callback = None
4142
self.estimated_tempo = None
4243
self.last_clock_time = None
44+
4345
logger.info("Opened MIDI input: %s" % self.midi.name)
4446

47+
self.notes_down: list[MidiNote] = []
48+
self.notes_down_dict: dict[int, MidiNote] = dict((n, None) for n in range(128))
49+
50+
self.callback: Callable = None
51+
self.on_note_on_handler: Callable = None
52+
self.on_note_off_handler: Callable = None
53+
self.on_control_change_handler: Callable = None
54+
self.on_aftertouch_handler: Callable = None
55+
self.on_polytouch_handler: Callable = None
56+
self.on_pitchbend_handler: Callable = None
57+
4558
@property
4659
def device_name(self):
4760
return self.midi.name
4861

49-
def _callback(self, message):
62+
def _mido_callback(self, message):
5063
"""
5164
Callback used by mido.
5265
Args:
@@ -88,7 +101,66 @@ def _callback(self, message):
88101
else:
89102
logger.warning("MIDI song position message received, but MIDI input cannot seek to arbitrary position")
90103

91-
elif message.type in ['note_on', 'note_off', 'control_change', 'pitchwheel']:
104+
elif message.type in ['note_on', 'note_off', 'control_change', 'pitchwheel', 'aftertouch', 'polytouch']:
105+
if message.type == 'note_off' or (message.type == 'note_on' and message.velocity == 0):
106+
#------------------------------------------------------------------------
107+
# Note off event
108+
#------------------------------------------------------------------------
109+
if self.notes_down_dict[message.note] is not None:
110+
note = self.notes_down_dict[message.note]
111+
self.notes_down_dict[message.note] = None
112+
self.notes_down.remove(note)
113+
if self.on_note_off_handler:
114+
self.on_note_off_handler(note)
115+
116+
elif message.type == 'note_on':
117+
#------------------------------------------------------------------------
118+
# Note on event
119+
#------------------------------------------------------------------------
120+
if self.notes_down_dict[message.note] is None:
121+
note = MidiNote(pitch=message.note,
122+
velocity=message.velocity,
123+
channel=message.channel)
124+
125+
self.notes_down.append(note)
126+
self.notes_down_dict[message.note] = note
127+
128+
if self.on_note_on_handler:
129+
self.on_note_on_handler(note)
130+
131+
elif message.type == 'control_change':
132+
#------------------------------------------------------------------------
133+
# Control change event
134+
#------------------------------------------------------------------------
135+
# TODO: This passes a mido message. Should be an isobar structure.
136+
if self.on_control_change_handler:
137+
self.on_control_change_handler(message)
138+
139+
elif message.type == 'pitchwheel':
140+
#------------------------------------------------------------------------
141+
# Pitch bend event
142+
#------------------------------------------------------------------------
143+
if self.on_pitchbend_handler:
144+
self.on_pitchbend_handler(message)
145+
146+
elif message.type == 'aftertouch':
147+
#------------------------------------------------------------------------
148+
# Aftertouch event
149+
#------------------------------------------------------------------------
150+
for note in self.notes_down:
151+
note.aftertouch = message.value
152+
if self.on_aftertouch_handler:
153+
self.on_aftertouch_handler(message)
154+
155+
elif message.type == 'polytouch':
156+
#------------------------------------------------------------------------
157+
# Polyphonic aftertouch event
158+
#------------------------------------------------------------------------
159+
note = self.notes_down_dict[message.note]
160+
note.aftertouch = message.value
161+
if self.on_polytouch_handler:
162+
self.on_polytouch_handler(message)
163+
92164
if self.callback:
93165
self.callback(message)
94166
else:
@@ -147,3 +219,48 @@ def poll(self):
147219

148220
def close(self):
149221
del self.midi
222+
223+
def set_note_on_handler(self, handler: Callable):
224+
"""
225+
Set the function to call when a note on event is received.
226+
The function should take a single argument, which is the MidiNote object.
227+
228+
Args:
229+
handler (Callable): The function to call.
230+
"""
231+
self.on_note_on_handler = handler
232+
233+
def set_note_off_handler(self, handler: Callable):
234+
"""
235+
Set the function to call when a note off event is received.
236+
237+
Args:
238+
handler (Callable): The function to call.
239+
"""
240+
self.on_note_off_handler = handler
241+
242+
def set_control_change_handler(self, handler: Callable):
243+
"""
244+
Set the function to call when a control change event is received.
245+
246+
Args:
247+
handler (Callable): The function to call.
248+
"""
249+
self.on_control_change_handler = handler
250+
251+
def set_polytouch_handler(self, handler: Callable):
252+
"""
253+
Set the function to call when a polyphonic aftertouch event is received.
254+
255+
Args:
256+
handler (Callable): The function to call.
257+
"""
258+
self.on_polytouch_handler = handler
259+
260+
def set_aftertouch_handler(self, handler: Callable):
261+
"""
262+
Set the function to call when an aftertouch event is received.
263+
Args:
264+
handler (Callable): The function to call.
265+
"""
266+
self.on_aftertouch_handler = handler

isobar/io/midi/output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def tick(self) -> None:
6464
self.midi.send(msg)
6565

6666
def note_on(self, note: int = 60, velocity: int = 64, channel: int = 0) -> None:
67-
log.debug("[midi] Note on (channel = %d, note = %d, velocity = %d)" % (channel, note, velocity))
67+
log.info("[midi] Note on (channel = %d, note = %d, velocity = %d)" % (channel, note, velocity))
6868
msg = mido.Message('note_on', note=int(note), velocity=int(velocity), channel=int(channel))
6969
self.midi.send(msg)
7070

0 commit comments

Comments
 (0)