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.

336 lines
12KB

  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. TICKS_PER_BEAT = 48
  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.fTimeSignature = (4,4)
  45. self.fTransportInfo = {
  46. "playing": False,
  47. "frame": 0,
  48. "bar": 0,
  49. "beat": 0,
  50. "tick": 0,
  51. "bpm": 120.0,
  52. }
  53. self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode)
  54. self.ui.act_edit_velocity.triggered.connect(self.slot_editVelocityMode)
  55. self.ui.act_edit_select_all.triggered.connect(self.slot_editSelectAll)
  56. self.ui.piano.midievent.connect(self.sendMsg)
  57. self.ui.piano.measureupdate.connect(self.updateMeasureBox)
  58. self.ui.piano.modeupdate.connect(self.ui.modeIndicator.changeMode)
  59. self.ui.piano.modeupdate.connect(self.slot_modeChanged)
  60. self.ui.timeSigBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  61. self.ui.measureBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  62. self.ui.defaultLengthBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  63. self.ui.quantizeBox.currentIndexChanged[int].connect(self.slot_paramChanged)
  64. self.ui.timeSigBox.currentIndexChanged[str].connect(self.slot_setTimeSignature)
  65. self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures)
  66. self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength)
  67. self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv)
  68. self.ui.hSlider.valueChanged.connect(self.ui.graphicsView.setZoomX)
  69. self.ui.vSlider.valueChanged.connect(self.ui.graphicsView.setZoomY)
  70. self.ui.graphicsView.setFocus()
  71. self.fIdleTimer = self.startTimer(30)
  72. self.setWindowTitle(self.fUiName)
  73. self.ready()
  74. def slot_editInsertMode(self):
  75. ev = QKeyEvent(QEvent.User, Qt.Key_F, Qt.NoModifier)
  76. self.ui.piano.keyPressEvent(ev)
  77. def slot_editVelocityMode(self):
  78. ev = QKeyEvent(QEvent.User, Qt.Key_D, Qt.NoModifier)
  79. self.ui.piano.keyPressEvent(ev)
  80. def slot_editSelectAll(self):
  81. ev = QKeyEvent(QEvent.User, Qt.Key_A, Qt.NoModifier)
  82. self.ui.piano.keyPressEvent(ev)
  83. def slot_modeChanged(self, mode):
  84. if mode == "insert_mode":
  85. self.ui.act_edit_insert.setChecked(True)
  86. self.ui.act_edit_velocity.setChecked(False)
  87. elif mode == "velocity_mode":
  88. self.ui.act_edit_insert.setChecked(False)
  89. self.ui.act_edit_velocity.setChecked(True)
  90. else:
  91. self.ui.act_edit_insert.setChecked(False)
  92. self.ui.act_edit_velocity.setChecked(False)
  93. def slot_paramChanged(self, index):
  94. sender = self.sender()
  95. if sender == self.ui.timeSigBox:
  96. param = 0
  97. elif sender == self.ui.measureBox:
  98. param = 1
  99. index += 1
  100. elif sender == self.ui.defaultLengthBox:
  101. param = 2
  102. elif sender == self.ui.quantizeBox:
  103. param = 3
  104. else:
  105. return
  106. self.sendControl(param, index)
  107. def slot_setTimeSignature(self, sigtext):
  108. try:
  109. timesig = tuple(map(float, sigtext.split('/')))
  110. except ValueError:
  111. return
  112. if len(timesig) != 2:
  113. return
  114. self.fTimeSignature = timesig
  115. self.ui.piano.setTimeSig(timesig)
  116. # -------------------------------------------------------------------
  117. # DSP Callbacks
  118. def dspParameterChanged(self, index, value):
  119. value = int(value)
  120. if index == 0: # TimeSig
  121. self.ui.timeSigBox.blockSignals(True)
  122. self.ui.timeSigBox.setCurrentIndex(value)
  123. self.slot_setTimeSignature(self.ui.timeSigBox.currentText())
  124. self.ui.timeSigBox.blockSignals(False)
  125. elif index == 1: # Measures
  126. self.ui.measureBox.blockSignals(True)
  127. self.ui.measureBox.setCurrentIndex(value-1)
  128. self.ui.piano.setMeasures(self.ui.measureBox.currentText())
  129. self.ui.measureBox.blockSignals(False)
  130. elif index == 2: # DefLength
  131. self.ui.defaultLengthBox.blockSignals(True)
  132. self.ui.defaultLengthBox.setCurrentIndex(value)
  133. self.ui.piano.setDefaultLength(self.ui.defaultLengthBox.currentText())
  134. self.ui.defaultLengthBox.blockSignals(False)
  135. elif index == 3: # Quantize
  136. self.ui.quantizeBox.blockSignals(True)
  137. self.ui.quantizeBox.setCurrentIndex(value)
  138. self.ui.piano.setQuantize(self.ui.quantizeBox.currentText())
  139. self.ui.quantizeBox.blockSignals(False)
  140. def dspStateChanged(self, key, value):
  141. pass
  142. # -------------------------------------------------------------------
  143. # ExternalUI Callbacks
  144. def uiShow(self):
  145. self.show()
  146. def uiFocus(self):
  147. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  148. self.show()
  149. self.raise_()
  150. self.activateWindow()
  151. def uiHide(self):
  152. self.hide()
  153. def uiQuit(self):
  154. self.closeExternalUI()
  155. self.close()
  156. app.quit()
  157. def uiTitleChanged(self, uiTitle):
  158. self.setWindowTitle(uiTitle)
  159. # -------------------------------------------------------------------
  160. # Qt events
  161. def timerEvent(self, event):
  162. if event.timerId() == self.fIdleTimer:
  163. self.idleExternalUI()
  164. QMainWindow.timerEvent(self, event)
  165. def closeEvent(self, event):
  166. self.closeExternalUI()
  167. QMainWindow.closeEvent(self, event)
  168. # there might be other qt windows open which will block the UI from quitting
  169. app.quit()
  170. # -------------------------------------------------------------------
  171. # Custom callback
  172. def updateMeasureBox(self, index):
  173. self.ui.measureBox.setCurrentIndex(index-1)
  174. def sendMsg(self, data):
  175. msg = data[0]
  176. if msg == "midievent-add":
  177. note, start, length, vel = data[1:5]
  178. note_start = start * self.TICKS_PER_BEAT
  179. note_stop = note_start + length * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT
  180. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  181. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  182. elif msg == "midievent-remove":
  183. note, start, length, vel = data[1:5]
  184. note_start = start * self.TICKS_PER_BEAT
  185. note_stop = note_start + length * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT
  186. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  187. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  188. def msgCallback(self, msg):
  189. msg = charPtrToString(msg)
  190. if msg == "midi-clear-all":
  191. # clear all notes
  192. self.ui.piano.clearNotes()
  193. elif msg == "midievent-add":
  194. # adds single midi event
  195. time = int(self.readlineblock())
  196. size = int(self.readlineblock())
  197. data = tuple(int(self.readlineblock()) for x in range(size))
  198. self.handleMidiEvent(time, size, data)
  199. elif msg == "transport":
  200. playing, frame, bar, beat, tick = tuple(int(i) for i in self.readlineblock().split(":"))
  201. bpm = float(self.readlineblock())
  202. playing = bool(int(playing))
  203. old_frame = self.fTransportInfo['frame']
  204. self.fTransportInfo = {
  205. "playing": playing,
  206. "frame": frame,
  207. "bar": bar,
  208. "beat": beat,
  209. "tick": tick,
  210. "bpm": bpm,
  211. "ticksPerBeat": self.TICKS_PER_BEAT,
  212. }
  213. if old_frame != frame:
  214. self.ui.piano.movePlayHead(self.fTransportInfo)
  215. elif msg == "parameters":
  216. timesig, measures, deflength, quantize = tuple(int(i) for i in self.readlineblock().split(":"))
  217. self.dspParameterChanged(0, timesig)
  218. self.dspParameterChanged(1, measures)
  219. self.dspParameterChanged(2, deflength)
  220. self.dspParameterChanged(3, quantize)
  221. else:
  222. ExternalUI.msgCallback(self, msg)
  223. # -------------------------------------------------------------------
  224. # Internal stuff
  225. def handleMidiEvent(self, time, size, data):
  226. #print("handleMidiEvent", time, size, data)
  227. status = MIDI_GET_STATUS_FROM_DATA(data)
  228. channel = MIDI_GET_CHANNEL_FROM_DATA(data)
  229. if status == MIDI_STATUS_NOTE_ON:
  230. note = data[1]
  231. velo = data[2]
  232. # append (channel, note, velo, time) for later
  233. self.fPendingNoteOns.append((channel, note, velo, time))
  234. elif status == MIDI_STATUS_NOTE_OFF:
  235. note = data[1]
  236. # find previous note-on that matches this note and channel
  237. for noteOnMsg in self.fPendingNoteOns:
  238. on_channel, on_note, on_velo, on_time = noteOnMsg
  239. if on_channel != channel:
  240. continue
  241. if on_note != note:
  242. continue
  243. # found it
  244. self.fPendingNoteOns.remove(noteOnMsg)
  245. break
  246. else:
  247. return
  248. self.ui.piano.drawNote(note,
  249. on_time/self.TICKS_PER_BEAT,
  250. (time-on_time)/self.TICKS_PER_BEAT/self.fTimeSignature[0], on_velo)
  251. #--------------- main ------------------
  252. if __name__ == '__main__':
  253. import resources_rc
  254. pathBinaries, pathResources = getPaths()
  255. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  256. gCarla.utils.set_process_name("MidiPattern")
  257. app = CarlaApplication("MidiPattern")
  258. gui = MidiPatternW()
  259. app.exit_exec()