Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

midipattern-ui 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # A piano roll viewer/editor
  4. # Copyright (C) 2012-2019 Filipe Coelho <falktx@falktx.com>
  5. # Copyright (C) 2014-2015 Perry Nguyen
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License as
  9. # published by the Free Software Foundation; either version 2 of
  10. # the License, or any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  18. # ------------------------------------------------------------------------------------------------------------
  19. # Imports (Global)
  20. from PyQt5.QtCore import pyqtSlot, Qt, QEvent
  21. from PyQt5.QtGui import QKeyEvent
  22. from PyQt5.QtWidgets import QMainWindow
  23. # ------------------------------------------------------------------------------------------------------------
  24. # Imports (Custom)
  25. from carla_shared import *
  26. from carla_utils import *
  27. from widgets.pianoroll import *
  28. import ui_midipattern
  29. # ------------------------------------------------------------------------------------------------------------
  30. # Imports (ExternalUI)
  31. from carla_app import CarlaApplication
  32. from externalui import ExternalUI
  33. # ------------------------------------------------------------------------------------------------------------
  34. class MidiPatternW(ExternalUI, QMainWindow):
  35. PPQ = 48.0
  36. def __init__(self):
  37. ExternalUI.__init__(self)
  38. QMainWindow.__init__(self)
  39. self.ui = ui_midipattern.Ui_MidiPatternW()
  40. self.ui.setupUi(self)
  41. self.ui.piano = self.ui.graphicsView.piano
  42. # to be filled with note-on events, while waiting for their matching note-off
  43. self.fPendingNoteOns = [] # (channel, note, velocity, time)
  44. self.fTransportInfo = {
  45. "playing": False,
  46. "frame": 0,
  47. "bar": 0,
  48. "beat": 0,
  49. "tick": 0,
  50. "bpm": 120.0,
  51. "sigNum": 4.0,
  52. "sigDenom": 4.0
  53. }
  54. self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode)
  55. self.ui.act_edit_velocity.triggered.connect(self.slot_editVelocityMode)
  56. self.ui.act_edit_select_all.triggered.connect(self.slot_editSelectAll)
  57. self.ui.piano.midievent.connect(self.sendMsg)
  58. self.ui.piano.measureupdate.connect(self.updateMeasureBox)
  59. self.ui.piano.modeupdate.connect(self.ui.modeIndicator.changeMode)
  60. self.ui.piano.modeupdate.connect(self.slot_modeChanged)
  61. self.ui.timeSigBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  62. self.ui.measureBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  63. self.ui.defaultLengthBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  64. self.ui.quantizeBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  65. self.ui.timeSigBox.currentIndexChanged[str].connect(self.ui.piano.setTimeSig)
  66. self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures)
  67. self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength)
  68. self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv)
  69. self.ui.hSlider.valueChanged.connect(self.ui.graphicsView.setZoomX)
  70. self.ui.vSlider.valueChanged.connect(self.ui.graphicsView.setZoomY)
  71. self.ui.graphicsView.setFocus()
  72. self.fIdleTimer = self.startTimer(30)
  73. self.setWindowTitle(self.fUiName)
  74. self.ready()
  75. def slot_editInsertMode(self):
  76. ev = QKeyEvent(QEvent.User, Qt.Key_F, Qt.NoModifier)
  77. self.ui.piano.keyPressEvent(ev)
  78. def slot_editVelocityMode(self):
  79. ev = QKeyEvent(QEvent.User, Qt.Key_D, Qt.NoModifier)
  80. self.ui.piano.keyPressEvent(ev)
  81. def slot_editSelectAll(self):
  82. ev = QKeyEvent(QEvent.User, Qt.Key_A, Qt.NoModifier)
  83. self.ui.piano.keyPressEvent(ev)
  84. def slot_modeChanged(self, mode):
  85. if mode == "insert_mode":
  86. self.ui.act_edit_insert.setChecked(True)
  87. self.ui.act_edit_velocity.setChecked(False)
  88. elif mode == "velocity_mode":
  89. self.ui.act_edit_insert.setChecked(False)
  90. self.ui.act_edit_velocity.setChecked(True)
  91. else:
  92. self.ui.act_edit_insert.setChecked(False)
  93. self.ui.act_edit_velocity.setChecked(False)
  94. def slot_paramChanged(self, index):
  95. sender = self.sender()
  96. if sender == self.ui.timeSigBox:
  97. param = 0
  98. elif sender == self.ui.measureBox:
  99. param = 1
  100. index += 1
  101. elif sender == self.ui.defaultLengthBox:
  102. param = 2
  103. elif sender == self.ui.quantizeBox:
  104. param = 3
  105. else:
  106. return
  107. self.sendControl(param, index)
  108. # -------------------------------------------------------------------
  109. # DSP Callbacks
  110. def dspParameterChanged(self, index, value):
  111. value = int(value)
  112. if index == 0: # TimeSig
  113. self.ui.timeSigBox.setCurrentIndex(value)
  114. elif index == 1: # Measures
  115. self.ui.measureBox.setCurrentIndex(value-1)
  116. elif index == 2: # DefLength
  117. self.ui.defaultLengthBox.setCurrentIndex(value)
  118. elif index == 3: # Quantize
  119. self.ui.quantizeBox.setCurrentIndex(value)
  120. def dspStateChanged(self, key, value):
  121. pass
  122. # -------------------------------------------------------------------
  123. # ExternalUI Callbacks
  124. def uiShow(self):
  125. self.show()
  126. def uiFocus(self):
  127. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  128. self.show()
  129. self.raise_()
  130. self.activateWindow()
  131. def uiHide(self):
  132. self.hide()
  133. def uiQuit(self):
  134. self.closeExternalUI()
  135. self.close()
  136. app.quit()
  137. def uiTitleChanged(self, uiTitle):
  138. self.setWindowTitle(uiTitle)
  139. # -------------------------------------------------------------------
  140. # Qt events
  141. def timerEvent(self, event):
  142. if event.timerId() == self.fIdleTimer:
  143. self.idleExternalUI()
  144. QMainWindow.timerEvent(self, event)
  145. def closeEvent(self, event):
  146. self.closeExternalUI()
  147. QMainWindow.closeEvent(self, event)
  148. # there might be other qt windows open which will block the UI from quitting
  149. app.quit()
  150. # -------------------------------------------------------------------
  151. # Custom callback
  152. def updateMeasureBox(self, index):
  153. self.measureBox.setCurrentIndex(index-1)
  154. def sendMsg(self, data):
  155. msg = data[0]
  156. if msg == "midievent-remove":
  157. note, start, length, vel = data[1:5]
  158. note_start = start * 60. / self.fTransportInfo["bpm"] * 4. / self.fTransportInfo["sigDenom"] * self.PPQ
  159. note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ
  160. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  161. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  162. elif msg == "midievent-add":
  163. note, start, length, vel = data[1:5]
  164. note_start = start * 60. / self.fTransportInfo["bpm"] * self.PPQ
  165. note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ
  166. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  167. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  168. def msgCallback(self, msg):
  169. msg = charPtrToString(msg)
  170. if msg == "midi-clear-all":
  171. # clear all notes
  172. self.ui.piano.clearNotes()
  173. elif msg == "midievent-add":
  174. # adds single midi event
  175. time = int(self.readlineblock())
  176. size = int(self.readlineblock())
  177. data = []
  178. for x in range(size):
  179. data.append(int(self.readlineblock()))
  180. self.handleMidiEvent(time, size, data)
  181. elif msg == "transport":
  182. playing = bool(self.readlineblock() == "true")
  183. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  184. bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")]
  185. if beat != self.fTransportInfo["beat"]:
  186. print(beat)
  187. old_frame = self.fTransportInfo['frame']
  188. self.fTransportInfo = {
  189. "playing": playing,
  190. "frame": frame,
  191. "bar": bar,
  192. "beat": beat,
  193. "tick": tick,
  194. "bpm": bpm,
  195. "sigNum": sigNum,
  196. "sigDenom": sigDenom
  197. }
  198. if old_frame != frame:
  199. self.ui.piano.movePlayHead(self.fTransportInfo)
  200. else:
  201. ExternalUI.msgCallback(self, msg)
  202. # -------------------------------------------------------------------
  203. # Internal stuff
  204. def handleMidiEvent(self, time, size, data):
  205. #print("Got MIDI Event on UI", time, size, data)
  206. # NOTE: for now time comes in frames, which might not be desirable
  207. # we'll convert it to a smaller value for now (seconds)
  208. # later on we can have time as PPQ or similar
  209. time /= self.PPQ
  210. status = MIDI_GET_STATUS_FROM_DATA(data)
  211. channel = MIDI_GET_CHANNEL_FROM_DATA(data)
  212. if status == MIDI_STATUS_NOTE_ON:
  213. note = data[1]
  214. velo = data[2]
  215. # append (channel, note, velo, time) for later
  216. self.fPendingNoteOns.append((channel, note, velo, time))
  217. elif status == MIDI_STATUS_NOTE_OFF:
  218. note = data[1]
  219. velo = data[2]
  220. # find previous note-on that matches this note and channel
  221. for noteOnMsg in self.fPendingNoteOns:
  222. channel_, note_, velo_, time_ = noteOnMsg
  223. if channel_ != channel:
  224. continue
  225. if note_ != note:
  226. continue
  227. # found it
  228. #print("{} {} {} {}\n".format(note, time_, time-time_, velo_))
  229. start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"]
  230. length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"]
  231. self.ui.piano.drawNote(note, start, length, velo_)
  232. # remove from list
  233. self.fPendingNoteOns.remove(noteOnMsg)
  234. break
  235. #--------------- main ------------------
  236. if __name__ == '__main__':
  237. import resources_rc
  238. pathBinaries, pathResources = getPaths()
  239. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  240. gCarla.utils.set_process_name("MidiPattern")
  241. app = CarlaApplication("MidiPattern")
  242. gui = MidiPatternW()
  243. app.exit_exec()