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.

421 lines
14KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Pixmap Keyboard, a custom Qt4 widget
  4. # Copyright (C) 2011-2012 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the COPYING file
  17. # Imports (Global)
  18. from PyQt4.QtCore import pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, SIGNAL, SLOT
  19. from PyQt4.QtGui import QFont, QPainter, QPixmap, QWidget
  20. midi_key2rect_map_horizontal = {
  21. '0': QRectF(0, 0, 18, 64), # C
  22. '1': QRectF(13, 0, 11, 42), # C#
  23. '2': QRectF(18, 0, 25, 64), # D
  24. '3': QRectF(37, 0, 11, 42), # D#
  25. '4': QRectF(42, 0, 18, 64), # E
  26. '5': QRectF(60, 0, 18, 64), # F
  27. '6': QRectF(73, 0, 11, 42), # F#
  28. '7': QRectF(78, 0, 25, 64), # G
  29. '8': QRectF(97, 0, 11, 42), # G#
  30. '9': QRectF(102, 0, 25, 64), # A
  31. '10': QRectF(121, 0, 11, 42), # A#
  32. '11': QRectF(126, 0, 18, 64) # B
  33. }
  34. midi_key2rect_map_vertical = {
  35. '11': QRectF(0, 0, 64, 18), # B
  36. '10': QRectF(0, 14, 42, 7), # A#
  37. '9': QRectF(0, 18, 64, 24), # A
  38. '8': QRectF(0, 38, 42, 7), # G#
  39. '7': QRectF(0, 42, 64, 24), # G
  40. '6': QRectF(0, 62, 42, 7), # F#
  41. '5': QRectF(0, 66, 64, 18), # F
  42. '4': QRectF(0, 84, 64, 18), # E
  43. '3': QRectF(0, 98, 42, 7), # D#
  44. '2': QRectF(0, 102, 64, 24), # D
  45. '1': QRectF(0, 122, 42, 7), # C#
  46. '0': QRectF(0, 126, 64, 18) # C
  47. }
  48. midi_keyboard2key_map = {
  49. # 3th octave
  50. '%i' % Qt.Key_Z: 48,
  51. '%i' % Qt.Key_S: 49,
  52. '%i' % Qt.Key_X: 50,
  53. '%i' % Qt.Key_D: 51,
  54. '%i' % Qt.Key_C: 52,
  55. '%i' % Qt.Key_V: 53,
  56. '%i' % Qt.Key_G: 54,
  57. '%i' % Qt.Key_B: 55,
  58. '%i' % Qt.Key_H: 56,
  59. '%i' % Qt.Key_N: 57,
  60. '%i' % Qt.Key_J: 58,
  61. '%i' % Qt.Key_M: 59,
  62. # 4th octave
  63. '%i' % Qt.Key_Q: 60,
  64. '%i' % Qt.Key_2: 61,
  65. '%i' % Qt.Key_W: 62,
  66. '%i' % Qt.Key_3: 63,
  67. '%i' % Qt.Key_E: 64,
  68. '%i' % Qt.Key_R: 65,
  69. '%i' % Qt.Key_5: 66,
  70. '%i' % Qt.Key_T: 67,
  71. '%i' % Qt.Key_6: 68,
  72. '%i' % Qt.Key_Y: 69,
  73. '%i' % Qt.Key_7: 70,
  74. '%i' % Qt.Key_U: 71,
  75. }
  76. # MIDI Keyboard, using a pixmap for painting
  77. class PixmapKeyboard(QWidget):
  78. # enum Color
  79. COLOR_CLASSIC = 0
  80. COLOR_ORANGE = 1
  81. # enum Orientation
  82. HORIZONTAL = 0
  83. VERTICAL = 1
  84. def __init__(self, parent):
  85. QWidget.__init__(self, parent)
  86. self.m_octaves = 6
  87. self.m_lastMouseNote = -1
  88. self.m_needsUpdate = False
  89. self.m_enabledKeys = []
  90. self.m_font = QFont("Monospace", 8, QFont.Normal)
  91. self.m_pixmap = QPixmap("")
  92. self.setCursor(Qt.PointingHandCursor)
  93. self.setMode(self.HORIZONTAL)
  94. def allNotesOff(self):
  95. self.m_enabledKeys = []
  96. self.m_needsUpdate = True
  97. QTimer.singleShot(0, self, SLOT("slot_updateOnce()"))
  98. self.emit(SIGNAL("notesOff()"))
  99. def sendNoteOn(self, note, sendSignal=True):
  100. if 0 <= note <= 127 and note not in self.m_enabledKeys:
  101. self.m_enabledKeys.append(note)
  102. if sendSignal:
  103. self.emit(SIGNAL("noteOn(int)"), note)
  104. self.m_needsUpdate = True
  105. QTimer.singleShot(0, self, SLOT("slot_updateOnce()"))
  106. if len(self.m_enabledKeys) == 1:
  107. self.emit(SIGNAL("notesOn()"))
  108. def sendNoteOff(self, note, sendSignal=True):
  109. if 0 <= note <= 127 and note in self.m_enabledKeys:
  110. self.m_enabledKeys.remove(note)
  111. if sendSignal:
  112. self.emit(SIGNAL("noteOff(int)"), note)
  113. self.m_needsUpdate = True
  114. QTimer.singleShot(0, self, SLOT("slot_updateOnce()"))
  115. if len(self.m_enabledKeys) == 0:
  116. self.emit(SIGNAL("notesOff()"))
  117. def setMode(self, mode, color=COLOR_ORANGE):
  118. if color == self.COLOR_CLASSIC:
  119. self.m_colorStr = "classic"
  120. elif color == self.COLOR_ORANGE:
  121. self.m_colorStr = "orange"
  122. else:
  123. qCritical("PixmapKeyboard::setMode(%i, %i) - invalid color" % (mode, color))
  124. return self.setMode(mode)
  125. if mode == self.HORIZONTAL:
  126. self.m_midi_map = midi_key2rect_map_horizontal
  127. self.m_pixmap.load(":/bitmaps/kbd_h_%s.png" % self.m_colorStr)
  128. self.m_pixmap_mode = self.HORIZONTAL
  129. self.p_width = self.m_pixmap.width()
  130. self.p_height = self.m_pixmap.height() / 2
  131. elif mode == self.VERTICAL:
  132. self.m_midi_map = midi_key2rect_map_vertical
  133. self.m_pixmap.load(":/bitmaps/kbd_v_%s.png" % self.m_colorStr)
  134. self.m_pixmap_mode = self.VERTICAL
  135. self.p_width = self.m_pixmap.width() / 2
  136. self.p_height = self.m_pixmap.height()
  137. else:
  138. qCritical("PixmapKeyboard::setMode(%i, %i) - invalid mode" % (mode, color))
  139. return self.setMode(self.HORIZONTAL)
  140. self.setOctaves(self.m_octaves)
  141. def setOctaves(self, octaves):
  142. if octaves < 1:
  143. octaves = 1
  144. elif octaves > 8:
  145. octaves = 8
  146. self.m_octaves = octaves
  147. if self.m_pixmap_mode == self.HORIZONTAL:
  148. self.setMinimumSize(self.p_width * self.m_octaves, self.p_height)
  149. self.setMaximumSize(self.p_width * self.m_octaves, self.p_height)
  150. elif self.m_pixmap_mode == self.VERTICAL:
  151. self.setMinimumSize(self.p_width, self.p_height * self.m_octaves)
  152. self.setMaximumSize(self.p_width, self.p_height * self.m_octaves)
  153. self.update()
  154. def handleMousePos(self, pos):
  155. if self.m_pixmap_mode == self.HORIZONTAL:
  156. if pos.x() < 0 or pos.x() > self.m_octaves * 144:
  157. return
  158. posX = pos.x() - 1
  159. octave = int(posX / self.p_width)
  160. n_pos = QPointF(posX % self.p_width, pos.y())
  161. elif self.m_pixmap_mode == self.VERTICAL:
  162. if pos.y() < 0 or pos.y() > self.m_octaves * 144:
  163. return
  164. posY = pos.y() - 1
  165. octave = int(self.m_octaves - posY / self.p_height)
  166. n_pos = QPointF(pos.x(), posY % self.p_height)
  167. else:
  168. return
  169. octave += 3
  170. if self.m_midi_map['1'].contains(n_pos): # C#
  171. note = 1
  172. elif self.m_midi_map['3'].contains(n_pos): # D#
  173. note = 3
  174. elif self.m_midi_map['6'].contains(n_pos): # F#
  175. note = 6
  176. elif self.m_midi_map['8'].contains(n_pos): # G#
  177. note = 8
  178. elif self.m_midi_map['10'].contains(n_pos):# A#
  179. note = 10
  180. elif self.m_midi_map['0'].contains(n_pos): # C
  181. note = 0
  182. elif self.m_midi_map['2'].contains(n_pos): # D
  183. note = 2
  184. elif self.m_midi_map['4'].contains(n_pos): # E
  185. note = 4
  186. elif self.m_midi_map['5'].contains(n_pos): # F
  187. note = 5
  188. elif self.m_midi_map['7'].contains(n_pos): # G
  189. note = 7
  190. elif self.m_midi_map['9'].contains(n_pos): # A
  191. note = 9
  192. elif self.m_midi_map['11'].contains(n_pos):# B
  193. note = 11
  194. else:
  195. note = -1
  196. if note != -1:
  197. note += octave * 12
  198. if self.m_lastMouseNote != note:
  199. self.sendNoteOff(self.m_lastMouseNote)
  200. self.sendNoteOn(note)
  201. else:
  202. self.sendNoteOff(self.m_lastMouseNote)
  203. self.m_lastMouseNote = note
  204. def keyPressEvent(self, event):
  205. if not event.isAutoRepeat():
  206. qKey = str(event.key())
  207. if qKey in midi_keyboard2key_map.keys():
  208. self.sendNoteOn(midi_keyboard2key_map.get(qKey))
  209. QWidget.keyPressEvent(self, event)
  210. def keyReleaseEvent(self, event):
  211. if not event.isAutoRepeat():
  212. qKey = str(event.key())
  213. if qKey in midi_keyboard2key_map.keys():
  214. self.sendNoteOff(midi_keyboard2key_map.get(qKey))
  215. QWidget.keyReleaseEvent(self, event)
  216. def mousePressEvent(self, event):
  217. self.m_lastMouseNote = -1
  218. self.handleMousePos(event.pos())
  219. self.setFocus()
  220. QWidget.mousePressEvent(self, event)
  221. def mouseMoveEvent(self, event):
  222. self.handleMousePos(event.pos())
  223. QWidget.mousePressEvent(self, event)
  224. def mouseReleaseEvent(self, event):
  225. if self.m_lastMouseNote != -1:
  226. self.sendNoteOff(self.m_lastMouseNote)
  227. self.m_lastMouseNote = -1
  228. QWidget.mouseReleaseEvent(self, event)
  229. def paintEvent(self, event):
  230. painter = QPainter(self)
  231. # -------------------------------------------------------------
  232. # Paint clean keys (as background)
  233. for octave in range(self.m_octaves):
  234. if self.m_pixmap_mode == self.HORIZONTAL:
  235. target = QRectF(self.p_width * octave, 0, self.p_width, self.p_height)
  236. elif self.m_pixmap_mode == self.VERTICAL:
  237. target = QRectF(0, self.p_height * octave, self.p_width, self.p_height)
  238. else:
  239. return
  240. source = QRectF(0, 0, self.p_width, self.p_height)
  241. painter.drawPixmap(target, self.m_pixmap, source)
  242. # -------------------------------------------------------------
  243. # Paint (white) pressed keys
  244. paintedWhite = False
  245. for i in range(len(self.m_enabledKeys)):
  246. note = self.m_enabledKeys[i]
  247. pos = self._getRectFromMidiNote(note)
  248. if self._isNoteBlack(note):
  249. continue
  250. if note < 36:
  251. # cannot paint this note
  252. continue
  253. elif note < 48:
  254. octave = 0
  255. elif note < 60:
  256. octave = 1
  257. elif note < 72:
  258. octave = 2
  259. elif note < 84:
  260. octave = 3
  261. elif note < 96:
  262. octave = 4
  263. elif note < 108:
  264. octave = 5
  265. elif note < 120:
  266. octave = 6
  267. elif note < 132:
  268. octave = 7
  269. else:
  270. # cannot paint this note either
  271. continue
  272. if self.m_pixmap_mode == self.VERTICAL:
  273. octave = self.m_octaves - octave - 1
  274. if self.m_pixmap_mode == self.HORIZONTAL:
  275. target = QRectF(pos.x() + (self.p_width * octave), 0, pos.width(), pos.height())
  276. source = QRectF(pos.x(), self.p_height, pos.width(), pos.height())
  277. elif self.m_pixmap_mode == self.VERTICAL:
  278. target = QRectF(pos.x(), pos.y() + (self.p_height * octave), pos.width(), pos.height())
  279. source = QRectF(self.p_width, pos.y(), pos.width(), pos.height())
  280. else:
  281. return
  282. paintedWhite = True
  283. painter.drawPixmap(target, self.m_pixmap, source)
  284. # -------------------------------------------------------------
  285. # Clear white keys border
  286. if paintedWhite:
  287. for octave in range(self.m_octaves):
  288. for note in (1, 3, 6, 8, 10):
  289. pos = self._getRectFromMidiNote(note)
  290. if self.m_pixmap_mode == self.HORIZONTAL:
  291. target = QRectF(pos.x() + (self.p_width * octave), 0, pos.width(), pos.height())
  292. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  293. elif self.m_pixmap_mode == self.VERTICAL:
  294. target = QRectF(pos.x(), pos.y() + (self.p_height * octave), pos.width(), pos.height())
  295. source = QRectF(0, pos.y(), pos.width(), pos.height())
  296. else:
  297. return
  298. painter.drawPixmap(target, self.m_pixmap, source)
  299. # -------------------------------------------------------------
  300. # Paint (black) pressed keys
  301. for i in range(len(self.m_enabledKeys)):
  302. note = self.m_enabledKeys[i]
  303. pos = self._getRectFromMidiNote(note)
  304. if not self._isNoteBlack(note):
  305. continue
  306. if note < 36:
  307. # cannot paint this note
  308. continue
  309. elif note < 48:
  310. octave = 0
  311. elif note < 60:
  312. octave = 1
  313. elif note < 72:
  314. octave = 2
  315. elif note < 84:
  316. octave = 3
  317. elif note < 96:
  318. octave = 4
  319. elif note < 108:
  320. octave = 5
  321. elif note < 120:
  322. octave = 6
  323. elif note < 132:
  324. octave = 7
  325. else:
  326. # cannot paint this note either
  327. continue
  328. if self.m_pixmap_mode == self.VERTICAL:
  329. octave = self.m_octaves - octave - 1
  330. if self.m_pixmap_mode == self.HORIZONTAL:
  331. target = QRectF(pos.x() + (self.p_width * octave), 0, pos.width(), pos.height())
  332. source = QRectF(pos.x(), self.p_height, pos.width(), pos.height())
  333. elif self.m_pixmap_mode == self.VERTICAL:
  334. target = QRectF(pos.x(), pos.y() + (self.p_height * octave), pos.width(), pos.height())
  335. source = QRectF(self.p_width, pos.y(), pos.width(), pos.height())
  336. else:
  337. return
  338. painter.drawPixmap(target, self.m_pixmap, source)
  339. # Paint C-number note info
  340. painter.setFont(self.m_font)
  341. painter.setPen(Qt.black)
  342. for i in range(self.m_octaves):
  343. if self.m_pixmap_mode == self.HORIZONTAL:
  344. painter.drawText(i * 144, 48, 18, 18, Qt.AlignCenter, "C%i" % int(i + 2))
  345. elif self.m_pixmap_mode == self.VERTICAL:
  346. painter.drawText(45, (self.m_octaves * 144) - (i * 144) - 16, 18, 18, Qt.AlignCenter, "C%i" % int(i + 2))
  347. @pyqtSlot()
  348. def slot_updateOnce(self):
  349. if self.m_needsUpdate:
  350. self.update()
  351. self.m_needsUpdate = False
  352. def _isNoteBlack(self, note):
  353. baseNote = note % 12
  354. return bool(baseNote in (1, 3, 6, 8, 10))
  355. def _getRectFromMidiNote(self, note):
  356. return self.m_midi_map.get(str(note % 12))