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.

342 lines
12KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # A piano roll viewer/editor
  4. # Copyright (C) 2012-2020 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.noteclicked.connect(self.sendTemporaryNote)
  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.slot_setTimeSignature)
  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. def slot_setTimeSignature(self, sigtext):
  109. try:
  110. timesig = tuple(map(float, sigtext.split('/')))
  111. except ValueError:
  112. return
  113. if len(timesig) != 2:
  114. return
  115. self.fTimeSignature = timesig
  116. self.ui.piano.setTimeSig(timesig)
  117. # -------------------------------------------------------------------
  118. # DSP Callbacks
  119. def dspParameterChanged(self, index, value):
  120. value = int(value)
  121. if index == 0: # TimeSig
  122. self.ui.timeSigBox.blockSignals(True)
  123. self.ui.timeSigBox.setCurrentIndex(value)
  124. self.slot_setTimeSignature(self.ui.timeSigBox.currentText())
  125. self.ui.timeSigBox.blockSignals(False)
  126. elif index == 1: # Measures
  127. self.ui.measureBox.blockSignals(True)
  128. self.ui.measureBox.setCurrentIndex(value-1)
  129. self.ui.piano.setMeasures(self.ui.measureBox.currentText())
  130. self.ui.measureBox.blockSignals(False)
  131. elif index == 2: # DefLength
  132. self.ui.defaultLengthBox.blockSignals(True)
  133. self.ui.defaultLengthBox.setCurrentIndex(value)
  134. self.ui.piano.setDefaultLength(self.ui.defaultLengthBox.currentText())
  135. self.ui.defaultLengthBox.blockSignals(False)
  136. elif index == 3: # Quantize
  137. self.ui.quantizeBox.blockSignals(True)
  138. self.ui.quantizeBox.setCurrentIndex(value)
  139. self.ui.piano.setQuantize(self.ui.quantizeBox.currentText())
  140. self.ui.quantizeBox.blockSignals(False)
  141. def dspStateChanged(self, key, value):
  142. pass
  143. # -------------------------------------------------------------------
  144. # ExternalUI Callbacks
  145. def uiShow(self):
  146. self.show()
  147. def uiFocus(self):
  148. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  149. self.show()
  150. self.raise_()
  151. self.activateWindow()
  152. def uiHide(self):
  153. self.hide()
  154. def uiQuit(self):
  155. self.closeExternalUI()
  156. self.close()
  157. app.quit()
  158. def uiTitleChanged(self, uiTitle):
  159. self.setWindowTitle(uiTitle)
  160. # -------------------------------------------------------------------
  161. # Qt events
  162. def timerEvent(self, event):
  163. if event.timerId() == self.fIdleTimer:
  164. self.idleExternalUI()
  165. QMainWindow.timerEvent(self, event)
  166. def closeEvent(self, event):
  167. self.closeExternalUI()
  168. QMainWindow.closeEvent(self, event)
  169. # there might be other qt windows open which will block the UI from quitting
  170. app.quit()
  171. # -------------------------------------------------------------------
  172. # Custom callback
  173. def updateMeasureBox(self, index):
  174. self.ui.measureBox.blockSignals(True)
  175. self.ui.measureBox.setCurrentIndex(index-1)
  176. self.ui.measureBox.blockSignals(False)
  177. def sendMsg(self, data):
  178. msg = data[0]
  179. if msg == "midievent-add":
  180. note, start, length, vel = data[1:5]
  181. note_start = start * self.TICKS_PER_BEAT
  182. note_stop = note_start + length * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT
  183. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  184. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  185. elif msg == "midievent-remove":
  186. note, start, length, vel = data[1:5]
  187. note_start = start * self.TICKS_PER_BEAT
  188. note_stop = note_start + length * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT
  189. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  190. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  191. def sendTemporaryNote(self, note, on):
  192. self.send(["midi-note", note, on])
  193. def msgCallback(self, msg):
  194. msg = charPtrToString(msg)
  195. if msg == "midi-clear-all":
  196. # clear all notes
  197. self.ui.piano.clearNotes()
  198. elif msg == "midievent-add":
  199. # adds single midi event
  200. time = self.readlineblock_int()
  201. size = self.readlineblock_int()
  202. data = tuple(self.readlineblock_int() for x in range(size))
  203. self.handleMidiEvent(time, size, data)
  204. elif msg == "transport":
  205. playing, frame, bar, beat, tick = tuple(int(i) for i in self.readlineblock().split(":"))
  206. bpm = self.readlineblock_float()
  207. playing = bool(int(playing))
  208. old_frame = self.fTransportInfo['frame']
  209. self.fTransportInfo = {
  210. "playing": playing,
  211. "frame": frame,
  212. "bar": bar,
  213. "beat": beat,
  214. "tick": tick,
  215. "bpm": bpm,
  216. "ticksPerBeat": self.TICKS_PER_BEAT,
  217. }
  218. if old_frame != frame:
  219. self.ui.piano.movePlayHead(self.fTransportInfo)
  220. elif msg == "parameters":
  221. timesig, measures, deflength, quantize = tuple(int(i) for i in self.readlineblock().split(":"))
  222. self.dspParameterChanged(0, timesig)
  223. self.dspParameterChanged(1, measures)
  224. self.dspParameterChanged(2, deflength)
  225. self.dspParameterChanged(3, quantize)
  226. else:
  227. ExternalUI.msgCallback(self, msg)
  228. # -------------------------------------------------------------------
  229. # Internal stuff
  230. def handleMidiEvent(self, time, size, data):
  231. #print("handleMidiEvent", time, size, data)
  232. status = MIDI_GET_STATUS_FROM_DATA(data)
  233. channel = MIDI_GET_CHANNEL_FROM_DATA(data)
  234. if status == MIDI_STATUS_NOTE_ON:
  235. note = data[1]
  236. velo = data[2]
  237. # append (channel, note, velo, time) for later
  238. self.fPendingNoteOns.append((channel, note, velo, time))
  239. elif status == MIDI_STATUS_NOTE_OFF:
  240. note = data[1]
  241. # find previous note-on that matches this note and channel
  242. for noteOnMsg in self.fPendingNoteOns:
  243. on_channel, on_note, on_velo, on_time = noteOnMsg
  244. if on_channel != channel:
  245. continue
  246. if on_note != note:
  247. continue
  248. # found it
  249. self.fPendingNoteOns.remove(noteOnMsg)
  250. break
  251. else:
  252. return
  253. self.ui.piano.drawNote(note,
  254. on_time/self.TICKS_PER_BEAT,
  255. (time-on_time)/self.TICKS_PER_BEAT/self.fTimeSignature[0], on_velo)
  256. #--------------- main ------------------
  257. if __name__ == '__main__':
  258. import resources_rc
  259. pathBinaries, _ = getPaths()
  260. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  261. gCarla.utils.set_process_name("MidiPattern")
  262. app = CarlaApplication("MidiPattern")
  263. gui = MidiPatternW()
  264. app.exit_exec()