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