2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-FileCopyrightText: 2014-2015 Perry Nguyen
4 # SPDX-License-Identifier: GPL-2.0-or-later
6 # ------------------------------------------------------------------------------------------------------------
9 from qt_compat import qt_config
12 from PyQt5.QtCore import pyqtSlot, QT_VERSION, Qt, QEvent
13 from PyQt5.QtGui import QKeyEvent
14 from PyQt5.QtWidgets import QMainWindow
16 from PyQt6.QtCore import pyqtSlot, QT_VERSION, Qt, QEvent
17 from PyQt6.QtGui import QKeyEvent
18 from PyQt6.QtWidgets import QMainWindow
20 # ------------------------------------------------------------------------------------------------------------
23 from carla_shared import *
24 from carla_utils import *
25 from widgets.pianoroll import *
29 # ------------------------------------------------------------------------------------------------------------
30 # Imports (ExternalUI)
32 from carla_app import CarlaApplication
33 from externalui import ExternalUI
35 # ------------------------------------------------------------------------------------------------------------
37 class MidiPatternW(ExternalUI, QMainWindow):
41 ExternalUI.__init__(self)
42 QMainWindow.__init__(self)
43 self.ui = ui_midipattern.Ui_MidiPatternW()
45 self.ui.piano = self.ui.graphicsView.piano
47 # to be filled with note-on events, while waiting for their matching note-off
48 self.fPendingNoteOns = [] # (channel, note, velocity, time)
50 self.fTimeSignature = (4,4)
51 self.fTransportInfo = {
60 self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode)
61 self.ui.act_edit_velocity.triggered.connect(self.slot_editVelocityMode)
62 self.ui.act_edit_select_all.triggered.connect(self.slot_editSelectAll)
64 self.ui.piano.midievent.connect(self.sendMsg)
65 self.ui.piano.noteclicked.connect(self.sendTemporaryNote)
66 self.ui.piano.measureupdate.connect(self.updateMeasureBox)
67 self.ui.piano.modeupdate.connect(self.ui.modeIndicator.changeMode)
68 self.ui.piano.modeupdate.connect(self.slot_modeChanged)
70 if QT_VERSION >= 0x60000:
71 self.ui.timeSigBox.currentIndexChanged.connect(self.slot_paramChanged)
72 self.ui.measureBox.currentIndexChanged.connect(self.slot_paramChanged)
73 self.ui.defaultLengthBox.currentIndexChanged.connect(self.slot_paramChanged)
74 self.ui.quantizeBox.currentIndexChanged.connect(self.slot_paramChanged)
75 self.ui.timeSigBox.currentTextChanged.connect(self.slot_setTimeSignature)
76 self.ui.measureBox.currentTextChanged.connect(self.ui.piano.setMeasures)
77 self.ui.defaultLengthBox.currentTextChanged.connect(self.ui.piano.setDefaultLength)
78 self.ui.quantizeBox.currentTextChanged.connect(self.ui.piano.setGridDiv)
80 self.ui.timeSigBox.currentIndexChanged[int].connect(self.slot_paramChanged)
81 self.ui.measureBox.currentIndexChanged[int].connect(self.slot_paramChanged)
82 self.ui.defaultLengthBox.currentIndexChanged[int].connect(self.slot_paramChanged)
83 self.ui.quantizeBox.currentIndexChanged[int].connect(self.slot_paramChanged)
84 self.ui.timeSigBox.currentIndexChanged[str].connect(self.slot_setTimeSignature)
85 self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures)
86 self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength)
87 self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv)
89 self.ui.hSlider.valueChanged.connect(self.ui.graphicsView.setZoomX)
90 self.ui.vSlider.valueChanged.connect(self.ui.graphicsView.setZoomY)
92 self.ui.graphicsView.setFocus()
94 self.fIdleTimer = self.startTimer(30)
95 self.setWindowTitle(self.fUiName)
98 def slot_editInsertMode(self):
99 ev = QKeyEvent(QEvent.User, Qt.Key_F, Qt.NoModifier)
100 self.ui.piano.keyPressEvent(ev)
102 def slot_editVelocityMode(self):
103 ev = QKeyEvent(QEvent.User, Qt.Key_D, Qt.NoModifier)
104 self.ui.piano.keyPressEvent(ev)
106 def slot_editSelectAll(self):
107 ev = QKeyEvent(QEvent.User, Qt.Key_A, Qt.NoModifier)
108 self.ui.piano.keyPressEvent(ev)
110 def slot_modeChanged(self, mode):
111 if mode == "insert_mode":
112 self.ui.act_edit_insert.setChecked(True)
113 self.ui.act_edit_velocity.setChecked(False)
114 elif mode == "velocity_mode":
115 self.ui.act_edit_insert.setChecked(False)
116 self.ui.act_edit_velocity.setChecked(True)
118 self.ui.act_edit_insert.setChecked(False)
119 self.ui.act_edit_velocity.setChecked(False)
121 def slot_paramChanged(self, index):
122 sender = self.sender()
124 if sender == self.ui.timeSigBox:
126 elif sender == self.ui.measureBox:
129 elif sender == self.ui.defaultLengthBox:
131 elif sender == self.ui.quantizeBox:
136 self.sendControl(param, index)
138 def slot_setTimeSignature(self, sigtext):
140 timesig = tuple(map(float, sigtext.split('/')))
144 if len(timesig) != 2:
147 self.fTimeSignature = timesig
148 self.ui.piano.setTimeSig(timesig)
150 # -------------------------------------------------------------------
153 def dspParameterChanged(self, index, value):
156 if index == 0: # TimeSig
157 self.ui.timeSigBox.blockSignals(True)
158 self.ui.timeSigBox.setCurrentIndex(value)
159 self.slot_setTimeSignature(self.ui.timeSigBox.currentText())
160 self.ui.timeSigBox.blockSignals(False)
162 elif index == 1: # Measures
163 self.ui.measureBox.blockSignals(True)
164 self.ui.measureBox.setCurrentIndex(value-1)
165 self.ui.piano.setMeasures(self.ui.measureBox.currentText())
166 self.ui.measureBox.blockSignals(False)
168 elif index == 2: # DefLength
169 self.ui.defaultLengthBox.blockSignals(True)
170 self.ui.defaultLengthBox.setCurrentIndex(value)
171 self.ui.piano.setDefaultLength(self.ui.defaultLengthBox.currentText())
172 self.ui.defaultLengthBox.blockSignals(False)
174 elif index == 3: # Quantize
175 self.ui.quantizeBox.blockSignals(True)
176 self.ui.quantizeBox.setCurrentIndex(value)
177 self.ui.piano.setQuantize(self.ui.quantizeBox.currentText())
178 self.ui.quantizeBox.blockSignals(False)
180 def dspStateChanged(self, key, value):
183 # -------------------------------------------------------------------
184 # ExternalUI Callbacks
190 self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
194 self.activateWindow()
200 self.closeExternalUI()
204 def uiTitleChanged(self, uiTitle):
205 self.setWindowTitle(uiTitle)
207 # -------------------------------------------------------------------
210 def timerEvent(self, event):
211 if event.timerId() == self.fIdleTimer:
212 self.idleExternalUI()
213 QMainWindow.timerEvent(self, event)
215 def closeEvent(self, event):
216 self.closeExternalUI()
217 QMainWindow.closeEvent(self, event)
219 # there might be other qt windows open which will block the UI from quitting
222 # -------------------------------------------------------------------
225 def updateMeasureBox(self, index):
226 self.ui.measureBox.blockSignals(True)
227 self.ui.measureBox.setCurrentIndex(index-1)
228 self.ui.measureBox.blockSignals(False)
230 def sendMsg(self, data):
232 if msg == "midievent-add":
233 note, start, length, vel = data[1:5]
234 note_start = start * self.TICKS_PER_BEAT
235 note_stop = note_start + length * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT
236 self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
237 self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
239 elif msg == "midievent-remove":
240 note, start, length, vel = data[1:5]
241 note_start = start * self.TICKS_PER_BEAT
242 note_stop = note_start + length * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT
243 self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
244 self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
246 def sendTemporaryNote(self, note, on):
247 self.send(["midi-note", note, on])
249 def msgCallback(self, msg):
250 msg = charPtrToString(msg)
252 if msg == "midi-clear-all":
254 self.ui.piano.clearNotes()
256 elif msg == "midievent-add":
257 # adds single midi event
258 time = self.readlineblock_int()
259 size = self.readlineblock_int()
260 data = tuple(self.readlineblock_int() for x in range(size))
262 self.handleMidiEvent(time, size, data)
264 elif msg == "transport":
265 playing, frame, bar, beat, tick = tuple(int(i) for i in self.readlineblock().split(":"))
266 bpm = self.readlineblock_float()
267 playing = bool(int(playing))
269 old_frame = self.fTransportInfo['frame']
271 self.fTransportInfo = {
278 "ticksPerBeat": self.TICKS_PER_BEAT,
281 if old_frame != frame:
282 self.ui.piano.movePlayHead(self.fTransportInfo)
284 elif msg == "parameters":
285 timesig, measures, deflength, quantize = tuple(int(i) for i in self.readlineblock().split(":"))
286 self.dspParameterChanged(0, timesig)
287 self.dspParameterChanged(1, measures)
288 self.dspParameterChanged(2, deflength)
289 self.dspParameterChanged(3, quantize)
292 ExternalUI.msgCallback(self, msg)
294 # -------------------------------------------------------------------
297 def handleMidiEvent(self, time, size, data):
298 #print("handleMidiEvent", time, size, data)
300 status = MIDI_GET_STATUS_FROM_DATA(data)
301 channel = MIDI_GET_CHANNEL_FROM_DATA(data)
303 if status == MIDI_STATUS_NOTE_ON:
307 # append (channel, note, velo, time) for later
308 self.fPendingNoteOns.append((channel, note, velo, time))
310 elif status == MIDI_STATUS_NOTE_OFF:
313 # find previous note-on that matches this note and channel
314 for noteOnMsg in self.fPendingNoteOns:
315 on_channel, on_note, on_velo, on_time = noteOnMsg
317 if on_channel != channel:
323 self.fPendingNoteOns.remove(noteOnMsg)
329 self.ui.piano.drawNote(note,
330 on_time/self.TICKS_PER_BEAT,
331 (time-on_time)/self.TICKS_PER_BEAT/self.fTimeSignature[0],
334 #--------------- main ------------------
335 if __name__ == '__main__':
338 pathBinaries, _ = getPaths()
339 gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
340 gCarla.utils.set_process_name("MidiPattern")
342 app = CarlaApplication("MidiPattern")