Cleanup
[carla.git] / source / frontend / midipattern-ui
blobafa61a2789b992f8b56eb278ce3c915a21f8a832
1 #!/usr/bin/env python3
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 # ------------------------------------------------------------------------------------------------------------
7 # Imports (Global)
9 from qt_compat import qt_config
11 if qt_config == 5:
12     from PyQt5.QtCore import pyqtSlot, QT_VERSION, Qt, QEvent
13     from PyQt5.QtGui import QKeyEvent
14     from PyQt5.QtWidgets import QMainWindow
15 elif qt_config == 6:
16     from PyQt6.QtCore import pyqtSlot, QT_VERSION, Qt, QEvent
17     from PyQt6.QtGui import QKeyEvent
18     from PyQt6.QtWidgets import QMainWindow
20 # ------------------------------------------------------------------------------------------------------------
21 # Imports (Custom)
23 from carla_shared import *
24 from carla_utils import *
25 from widgets.pianoroll import *
27 import ui_midipattern
29 # ------------------------------------------------------------------------------------------------------------
30 # Imports (ExternalUI)
32 from carla_app import CarlaApplication
33 from externalui import ExternalUI
35 # ------------------------------------------------------------------------------------------------------------
37 class MidiPatternW(ExternalUI, QMainWindow):
38     TICKS_PER_BEAT = 48
40     def __init__(self):
41         ExternalUI.__init__(self)
42         QMainWindow.__init__(self)
43         self.ui = ui_midipattern.Ui_MidiPatternW()
44         self.ui.setupUi(self)
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 = {
52             "playing": False,
53             "frame": 0,
54             "bar": 0,
55             "beat": 0,
56             "tick": 0,
57             "bpm": 120.0,
58         }
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)
79         else:
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)
96         self.ready()
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)
117         else:
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:
125             param = 0
126         elif sender == self.ui.measureBox:
127             param = 1
128             index += 1
129         elif sender == self.ui.defaultLengthBox:
130             param = 2
131         elif sender == self.ui.quantizeBox:
132             param = 3
133         else:
134             return
136         self.sendControl(param, index)
138     def slot_setTimeSignature(self, sigtext):
139         try:
140            timesig = tuple(map(float, sigtext.split('/')))
141         except ValueError:
142             return
144         if len(timesig) != 2:
145             return
147         self.fTimeSignature = timesig
148         self.ui.piano.setTimeSig(timesig)
150     # -------------------------------------------------------------------
151     # DSP Callbacks
153     def dspParameterChanged(self, index, value):
154         value = int(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):
181         pass
183     # -------------------------------------------------------------------
184     # ExternalUI Callbacks
186     def uiShow(self):
187         self.show()
189     def uiFocus(self):
190         self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
191         self.show()
193         self.raise_()
194         self.activateWindow()
196     def uiHide(self):
197         self.hide()
199     def uiQuit(self):
200         self.closeExternalUI()
201         self.close()
202         app.quit()
204     def uiTitleChanged(self, uiTitle):
205         self.setWindowTitle(uiTitle)
207     # -------------------------------------------------------------------
208     # Qt events
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
220         app.quit()
222     # -------------------------------------------------------------------
223     # Custom callback
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):
231         msg = data[0]
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":
253             # clear all notes
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 = {
272                 "playing": playing,
273                 "frame": frame,
274                 "bar": bar,
275                 "beat": beat,
276                 "tick": tick,
277                 "bpm": bpm,
278                 "ticksPerBeat": self.TICKS_PER_BEAT,
279             }
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)
291         else:
292             ExternalUI.msgCallback(self, msg)
294     # -------------------------------------------------------------------
295     # Internal stuff
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:
304             note = data[1]
305             velo = data[2]
307             # append (channel, note, velo, time) for later
308             self.fPendingNoteOns.append((channel, note, velo, time))
310         elif status == MIDI_STATUS_NOTE_OFF:
311             note = data[1]
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:
318                     continue
319                 if on_note != note:
320                     continue
322                 # found it
323                 self.fPendingNoteOns.remove(noteOnMsg)
324                 break
326             else:
327                 return
329             self.ui.piano.drawNote(note,
330                                    on_time/self.TICKS_PER_BEAT,
331                                    (time-on_time)/self.TICKS_PER_BEAT/self.fTimeSignature[0],
332                                    on_velo)
334 #--------------- main ------------------
335 if __name__ == '__main__':
336     import resources_rc
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")
343     gui = MidiPatternW()
344     app.exit_exec()