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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
|
"""
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 re
from PyQt5.QtGui import QTextCursor
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 targetchannel == 0 or channel == targetchannel-1: # '0' captures all
# midi channels start at 1 for humans and 0 for programs
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.print_or_replace(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.print_or_replace(self._chord.output(self.widget().relativemode(), self._language))
self._activenotes = 0 # reset in case it was negative
self._chord = None
def print_or_replace(self, text):
view = self.widget().mainwindow()
cursor = view.textCursor()
if self.widget().repitchmode():
music = cursor.document().toPlainText()[cursor.position() : ]
ly_reg_expr = r'(?<![a-zA-Z#_^\-\\])[a-ps-zA-PS-Z]{1,3}(?![a-zA-Z])[\'\,]*'\
'|'\
r'(?<![<\\])<[^<>]*>(?!>)'
notes = re.search(ly_reg_expr,music)
if notes != None :
start = cursor.position() + notes.start()
end = cursor.position() + notes.end()
cursor.beginEditBlock()
cursor.setPosition(start)
cursor.setPosition(end, QTextCursor.KeepAnchor)
cursor.insertText(text)
cursor.endEditBlock()
view.setTextCursor(cursor)
else:
# 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
|