1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
"""
MIDI input package
provides a dock which allows to capture midi events and insert notes
- input is static, not dynamic
- special midi events (e. g. damper pedal) can modify notes (e. g. duration)
or insert elements (e. g. slurs)
current limitations:
- special events not implemented yet
TODO:
dynamic input
"""
import time
import weakref
from PyQt5.QtCore import QObject, QSettings, QThread, pyqtSignal
import midihub
import midifile.event
import midifile.parser
import documentinfo
from . import elements
class MidiIn(object):
def __init__(self, widget):
self._widget = weakref.ref(widget)
self._portmidiinput = None
self._listener = None
self._chord = None
def __del__(self):
if isinstance(self._listener, Listener):
self.capturestop()
def widget(self):
return self._widget()
def open(self):
s = QSettings()
self._portname = s.value("midi/midi/input_port", midihub.default_input(), str)
self._pollingtime = s.value("midi/polling_time", 10, int)
self._portmidiinput = midihub.input_by_name(self._portname)
self._listener = Listener(self._portmidiinput, self._pollingtime)
self._listener.NoteEventSignal.connect(self.analyzeevent)
def close(self):
# self._portmidiinput.close()
# this will end in segfault with pyportmidi 0.0.7 in ubuntu
# see https://groups.google.com/d/msg/pygame-mirror-on-google-groups/UA16GbFsUDE/RkYxb9SzZFwJ
# so we cleanup ourself and invoke __dealloc__() by garbage collection
# so discard any reference to a pypm.Input instance
if self._portmidiinput:
self._portmidiinput._input = None
self._portmidiinput = None
self._listener = None
def capture(self):
if not self._portmidiinput:
self.open()
if not self._portmidiinput:
return
doc = self.widget().mainwindow().currentDocument()
self._language = documentinfo.docinfo(doc).language() or 'nederlands'
self._activenotes = 0
self._listener.start()
def capturestop(self):
self._listener.stop()
if not self._listener.isFinished():
self._listener.wait()
self._activenotes = 0
self.close()
def analyzeevent(self, event):
if isinstance(event, midifile.event.NoteEvent):
self.noteevent(event.type, event.channel, event.note, event.value)
def noteevent(self, notetype, channel, notenumber, value):
targetchannel = self.widget().channel()
if channel == targetchannel or targetchannel == 0: # '0' captures all
if notetype == 9 and value > 0: # note on with velocity > 0
notemapping = elements.NoteMapping(self.widget().keysignature(), self.widget().accidentals()=='sharps')
note = elements.Note(notenumber, notemapping)
if self.widget().chordmode():
if not self._chord: # no Chord instance?
self._chord = elements.Chord()
self._chord.add(note)
self._activenotes += 1
else:
self.printwithspace(note.output(self.widget().relativemode(), self._language))
elif (notetype == 8 or (notetype == 9 and value == 0)) and self.widget().chordmode():
self._activenotes -= 1
if self._activenotes <= 0: # activenotes could get negative under strange conditions
if self._chord:
self.printwithspace(self._chord.output(self.widget().relativemode(), self._language))
self._activenotes = 0 # reset in case it was negative
self._chord = None
def printwithspace(self, text):
cursor = self.widget().mainwindow().textCursor()
# check if there is a space before cursor or beginning of line
posinblock = cursor.position() - cursor.block().position()
charbeforecursor = cursor.block().text()[posinblock-1:posinblock]
if charbeforecursor.isspace() or cursor.atBlockStart():
cursor.insertText(text)
else:
cursor.insertText(' ' + text)
class Listener(QThread):
NoteEventSignal = pyqtSignal(midifile.event.NoteEvent)
def __init__(self, portmidiinput, pollingtime):
QThread.__init__(self)
self._portmidiinput = portmidiinput
self._pollingtime = pollingtime
def run(self):
self._capturing = True
while self._capturing:
while not self._portmidiinput.poll():
time.sleep(self._pollingtime/1000.)
if not self._capturing:
break
if not self._capturing:
break
data = self._portmidiinput.read(1)
# midifile.parser.parse_midi_events is a generator which expects a long "byte string" from a file,
# so we feed it one. But since it's just one event, we only need the first "generated" element.
# First byte is time, which is unnecessary in our case, so we feed a dummy byte 77
# and strip output by just using [1]. 77 is chosen randomly ;)
s = bytearray([77, data[0][0][0], data[0][0][1], data[0][0][2], data[0][0][3]])
event = next(midifile.parser.parse_midi_events(s))[1]
if isinstance(event,midifile.event.NoteEvent):
self.NoteEventSignal.emit(event)
def stop(self):
self._capturing = False
|