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.

483 lines
16KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Pixmap Keyboard, a custom Qt4 widget
  4. # Copyright (C) 2011-2014 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or 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 doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. from PyQt4.QtCore import pyqtSignal, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, QSize
  20. from PyQt4.QtGui import QColor, QFont, QPainter, QPixmap, QScrollArea, QWidget
  21. # ------------------------------------------------------------------------------------------------------------
  22. kMidiKey2RectMapHorizontal = {
  23. '0': QRectF(0, 0, 17, 59), # C
  24. '1': QRectF(13, 0, 8, 41), # C#
  25. '2': QRectF(17, 0, 17, 59), # D
  26. '3': QRectF(31, 0, 8, 41), # D#
  27. '4': QRectF(34, 0, 17, 59), # E
  28. '5': QRectF(51, 0, 17, 59), # F
  29. '6': QRectF(63, 0, 8, 41), # F#
  30. '7': QRectF(68, 0, 17, 59), # G
  31. '8': QRectF(81, 0, 8, 41), # G#
  32. '9': QRectF(85, 0, 17, 59), # A
  33. '10': QRectF(99, 0, 8, 41), # A#
  34. '11': QRectF(102, 0, 17, 59) # B
  35. }
  36. kMidiKey2RectMapVertical = {
  37. '11': QRectF(0, 0, 59, 17), # B
  38. '10': QRectF(0, 11, 41, 8), # A#
  39. '9': QRectF(0, 16, 59, 17), # A
  40. '8': QRectF(0, 29, 41, 8), # G#
  41. '7': QRectF(0, 33, 59, 18), # G
  42. '6': QRectF(0, 47, 41, 8), # F#
  43. '5': QRectF(0, 50, 59, 17), # F
  44. '4': QRectF(0, 67, 59, 17), # E
  45. '3': QRectF(0, 79, 41, 8), # D#
  46. '2': QRectF(0, 84, 59, 17), # D
  47. '1': QRectF(0, 97, 41, 8), # C#
  48. '0': QRectF(0, 101, 59, 17) # C
  49. }
  50. kMidiKeyboard2KeyMap = {
  51. # 3th octave
  52. '%i' % Qt.Key_Z: 48,
  53. '%i' % Qt.Key_S: 49,
  54. '%i' % Qt.Key_X: 50,
  55. '%i' % Qt.Key_D: 51,
  56. '%i' % Qt.Key_C: 52,
  57. '%i' % Qt.Key_V: 53,
  58. '%i' % Qt.Key_G: 54,
  59. '%i' % Qt.Key_B: 55,
  60. '%i' % Qt.Key_H: 56,
  61. '%i' % Qt.Key_N: 57,
  62. '%i' % Qt.Key_J: 58,
  63. '%i' % Qt.Key_M: 59,
  64. # 4th octave
  65. '%i' % Qt.Key_Q: 60,
  66. '%i' % Qt.Key_2: 61,
  67. '%i' % Qt.Key_W: 62,
  68. '%i' % Qt.Key_3: 63,
  69. '%i' % Qt.Key_E: 64,
  70. '%i' % Qt.Key_R: 65,
  71. '%i' % Qt.Key_5: 66,
  72. '%i' % Qt.Key_T: 67,
  73. '%i' % Qt.Key_6: 68,
  74. '%i' % Qt.Key_Y: 69,
  75. '%i' % Qt.Key_7: 70,
  76. '%i' % Qt.Key_U: 71,
  77. }
  78. kBlackNotes = (1, 3, 6, 8, 10)
  79. # ------------------------------------------------------------------------------------------------------------
  80. # MIDI Keyboard, using a pixmap for painting
  81. class PixmapKeyboard(QWidget):
  82. # enum Orientation
  83. HORIZONTAL = 0
  84. VERTICAL = 1
  85. # signals
  86. noteOn = pyqtSignal(int)
  87. noteOff = pyqtSignal(int)
  88. notesOn = pyqtSignal()
  89. notesOff = pyqtSignal()
  90. def __init__(self, parent):
  91. QWidget.__init__(self, parent)
  92. self.fOctaves = 6
  93. self.fLastMouseNote = -1
  94. self.fEnabledKeys = []
  95. self.fFont = QFont("Monospace", 7, QFont.Normal)
  96. self.fPixmap = QPixmap("")
  97. self.setCursor(Qt.PointingHandCursor)
  98. self.setMode(self.HORIZONTAL)
  99. def allNotesOff(self, sendSignal=True):
  100. self.fEnabledKeys = []
  101. if sendSignal:
  102. self.notesOff.emit()
  103. self.update()
  104. def sendNoteOn(self, note, sendSignal=True):
  105. if 0 <= note <= 127 and note not in self.fEnabledKeys:
  106. self.fEnabledKeys.append(note)
  107. if sendSignal:
  108. self.noteOn.emit(note)
  109. self.update()
  110. if len(self.fEnabledKeys) == 1:
  111. self.notesOn.emit()
  112. def sendNoteOff(self, note, sendSignal=True):
  113. if 0 <= note <= 127 and note in self.fEnabledKeys:
  114. self.fEnabledKeys.remove(note)
  115. if sendSignal:
  116. self.noteOff.emit(note)
  117. self.update()
  118. if len(self.fEnabledKeys) == 0:
  119. self.notesOff.emit()
  120. def setMode(self, mode):
  121. if mode == self.HORIZONTAL:
  122. self.fMidiMap = kMidiKey2RectMapHorizontal
  123. self.fPixmap.load(":/bitmaps/kbd_h_dark.png")
  124. self.fPixmapMode = self.HORIZONTAL
  125. self.fWidth = self.fPixmap.width()
  126. self.fHeight = self.fPixmap.height() / 2
  127. elif mode == self.VERTICAL:
  128. self.fMidiMap = kMidiKey2RectMapVertical
  129. self.fPixmap.load(":/bitmaps/kbd_v_dark.png")
  130. self.fPixmapMode = self.VERTICAL
  131. self.fWidth = self.fPixmap.width() / 2
  132. self.fHeight = self.fPixmap.height()
  133. else:
  134. qCritical("PixmapKeyboard::setMode(%i) - invalid mode" % mode)
  135. return self.setMode(self.HORIZONTAL)
  136. self.setOctaves(self.fOctaves)
  137. def setOctaves(self, octaves):
  138. if octaves < 1:
  139. octaves = 1
  140. elif octaves > 10:
  141. octaves = 10
  142. self.fOctaves = octaves
  143. if self.fPixmapMode == self.HORIZONTAL:
  144. self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight)
  145. self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight)
  146. elif self.fPixmapMode == self.VERTICAL:
  147. self.setMinimumSize(self.fWidth, self.fHeight * self.fOctaves)
  148. self.setMaximumSize(self.fWidth, self.fHeight * self.fOctaves)
  149. self.update()
  150. def handleMousePos(self, pos):
  151. if self.fPixmapMode == self.HORIZONTAL:
  152. if pos.x() < 0 or pos.x() > self.fOctaves * 119:
  153. return
  154. octave = int(pos.x() / self.fWidth)
  155. keyPos = QPointF(pos.x() % self.fWidth, pos.y())
  156. elif self.fPixmapMode == self.VERTICAL:
  157. if pos.y() < 0 or pos.y() > self.fOctaves * 119:
  158. return
  159. octave = int(self.fOctaves - pos.y() / self.fHeight)
  160. keyPos = QPointF(pos.x(), (pos.y()-1) % self.fHeight)
  161. else:
  162. return
  163. if self.fMidiMap['1'].contains(keyPos): # C#
  164. note = 1
  165. elif self.fMidiMap['3'].contains(keyPos): # D#
  166. note = 3
  167. elif self.fMidiMap['6'].contains(keyPos): # F#
  168. note = 6
  169. elif self.fMidiMap['8'].contains(keyPos): # G#
  170. note = 8
  171. elif self.fMidiMap['10'].contains(keyPos):# A#
  172. note = 10
  173. elif self.fMidiMap['0'].contains(keyPos): # C
  174. note = 0
  175. elif self.fMidiMap['2'].contains(keyPos): # D
  176. note = 2
  177. elif self.fMidiMap['4'].contains(keyPos): # E
  178. note = 4
  179. elif self.fMidiMap['5'].contains(keyPos): # F
  180. note = 5
  181. elif self.fMidiMap['7'].contains(keyPos): # G
  182. note = 7
  183. elif self.fMidiMap['9'].contains(keyPos): # A
  184. note = 9
  185. elif self.fMidiMap['11'].contains(keyPos):# B
  186. note = 11
  187. else:
  188. note = -1
  189. if note != -1:
  190. note += octave * 12
  191. if self.fLastMouseNote != note:
  192. self.sendNoteOff(self.fLastMouseNote)
  193. self.sendNoteOn(note)
  194. elif self.fLastMouseNote != -1:
  195. self.sendNoteOff(self.fLastMouseNote)
  196. self.fLastMouseNote = note
  197. def minimumSizeHint(self):
  198. return QSize(self.fWidth, self.fHeight)
  199. def sizeHint(self):
  200. if self.fPixmapMode == self.HORIZONTAL:
  201. return QSize(self.fWidth * self.fOctaves, self.fHeight)
  202. elif self.fPixmapMode == self.VERTICAL:
  203. return QSize(self.fWidth, self.fHeight * self.fOctaves)
  204. else:
  205. return QSize(self.fWidth, self.fHeight)
  206. def keyPressEvent(self, event):
  207. if not event.isAutoRepeat():
  208. qKey = str(event.key())
  209. if qKey in kMidiKeyboard2KeyMap.keys():
  210. self.sendNoteOn(kMidiKeyboard2KeyMap.get(qKey))
  211. QWidget.keyPressEvent(self, event)
  212. def keyReleaseEvent(self, event):
  213. if not event.isAutoRepeat():
  214. qKey = str(event.key())
  215. if qKey in kMidiKeyboard2KeyMap.keys():
  216. self.sendNoteOff(kMidiKeyboard2KeyMap.get(qKey))
  217. QWidget.keyReleaseEvent(self, event)
  218. def mousePressEvent(self, event):
  219. self.fLastMouseNote = -1
  220. self.handleMousePos(event.pos())
  221. self.setFocus()
  222. QWidget.mousePressEvent(self, event)
  223. def mouseMoveEvent(self, event):
  224. self.handleMousePos(event.pos())
  225. QWidget.mouseMoveEvent(self, event)
  226. def mouseReleaseEvent(self, event):
  227. if self.fLastMouseNote != -1:
  228. self.sendNoteOff(self.fLastMouseNote)
  229. self.fLastMouseNote = -1
  230. QWidget.mouseReleaseEvent(self, event)
  231. def paintEvent(self, event):
  232. painter = QPainter(self)
  233. event.accept()
  234. # -------------------------------------------------------------
  235. # Paint clean keys (as background)
  236. for octave in range(self.fOctaves):
  237. if self.fPixmapMode == self.HORIZONTAL:
  238. target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight)
  239. elif self.fPixmapMode == self.VERTICAL:
  240. target = QRectF(0, self.fHeight * octave, self.fWidth, self.fHeight)
  241. else:
  242. return
  243. source = QRectF(0, 0, self.fWidth, self.fHeight)
  244. painter.drawPixmap(target, self.fPixmap, source)
  245. if not self.isEnabled():
  246. painter.setBrush(QColor(0, 0, 0, 150))
  247. painter.setPen(QColor(0, 0, 0, 150))
  248. painter.drawRect(0, 0, self.width(), self.height())
  249. return
  250. # -------------------------------------------------------------
  251. # Paint (white) pressed keys
  252. paintedWhite = False
  253. for note in self.fEnabledKeys:
  254. pos = self._getRectFromMidiNote(note)
  255. if self._isNoteBlack(note):
  256. continue
  257. if note < 12:
  258. octave = 0
  259. elif note < 24:
  260. octave = 1
  261. elif note < 36:
  262. octave = 2
  263. elif note < 48:
  264. octave = 3
  265. elif note < 60:
  266. octave = 4
  267. elif note < 72:
  268. octave = 5
  269. elif note < 84:
  270. octave = 6
  271. elif note < 96:
  272. octave = 7
  273. elif note < 108:
  274. octave = 8
  275. elif note < 120:
  276. octave = 9
  277. elif note < 132:
  278. octave = 10
  279. else:
  280. # cannot paint this note
  281. continue
  282. if self.fPixmapMode == self.VERTICAL:
  283. octave = self.fOctaves - octave - 1
  284. if self.fPixmapMode == self.HORIZONTAL:
  285. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  286. source = QRectF(pos.x(), self.fHeight, pos.width(), pos.height())
  287. elif self.fPixmapMode == self.VERTICAL:
  288. target = QRectF(pos.x(), pos.y() + (self.fHeight * octave), pos.width(), pos.height())
  289. source = QRectF(self.fWidth, pos.y(), pos.width(), pos.height())
  290. else:
  291. return
  292. paintedWhite = True
  293. painter.drawPixmap(target, self.fPixmap, source)
  294. # -------------------------------------------------------------
  295. # Clear white keys border
  296. if paintedWhite:
  297. for octave in range(self.fOctaves):
  298. for note in kBlackNotes:
  299. pos = self._getRectFromMidiNote(note)
  300. if self.fPixmapMode == self.HORIZONTAL:
  301. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  302. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  303. elif self.fPixmapMode == self.VERTICAL:
  304. target = QRectF(pos.x(), pos.y() + (self.fHeight * octave), pos.width(), pos.height())
  305. source = QRectF(0, pos.y(), pos.width(), pos.height())
  306. else:
  307. return
  308. painter.drawPixmap(target, self.fPixmap, source)
  309. # -------------------------------------------------------------
  310. # Paint (black) pressed keys
  311. for note in self.fEnabledKeys:
  312. pos = self._getRectFromMidiNote(note)
  313. if not self._isNoteBlack(note):
  314. continue
  315. if note < 12:
  316. octave = 0
  317. elif note < 24:
  318. octave = 1
  319. elif note < 36:
  320. octave = 2
  321. elif note < 48:
  322. octave = 3
  323. elif note < 60:
  324. octave = 4
  325. elif note < 72:
  326. octave = 5
  327. elif note < 84:
  328. octave = 6
  329. elif note < 96:
  330. octave = 7
  331. elif note < 108:
  332. octave = 8
  333. elif note < 120:
  334. octave = 9
  335. elif note < 132:
  336. octave = 10
  337. else:
  338. # cannot paint this note
  339. continue
  340. if self.fPixmapMode == self.VERTICAL:
  341. octave = self.fOctaves - octave - 1
  342. if self.fPixmapMode == self.HORIZONTAL:
  343. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  344. source = QRectF(pos.x(), self.fHeight, pos.width(), pos.height())
  345. elif self.fPixmapMode == self.VERTICAL:
  346. target = QRectF(pos.x(), pos.y() + (self.fHeight * octave), pos.width(), pos.height())
  347. source = QRectF(self.fWidth, pos.y(), pos.width(), pos.height())
  348. else:
  349. return
  350. painter.drawPixmap(target, self.fPixmap, source)
  351. # Paint C-number note info
  352. painter.setFont(self.fFont)
  353. painter.setPen(Qt.black)
  354. for i in range(self.fOctaves):
  355. if self.fPixmapMode == self.HORIZONTAL:
  356. painter.drawText(i * 119 + (2 if i == 0 else 1), 45, 16, 16, Qt.AlignCenter, "C%i" % (i-1))
  357. elif self.fPixmapMode == self.VERTICAL:
  358. painter.drawText(44, (self.fOctaves * 119) - (i * 119) - 16, 16, 16, Qt.AlignCenter, "C%i" % (i-1))
  359. def _isNoteBlack(self, note):
  360. baseNote = note % 12
  361. return bool(baseNote in kBlackNotes)
  362. def _getRectFromMidiNote(self, note):
  363. baseNote = note % 12
  364. return self.fMidiMap.get(str(baseNote))
  365. # ------------------------------------------------------------------------------------------------------------
  366. # Horizontal scroll area for keyboard
  367. class PixmapKeyboardHArea(QScrollArea):
  368. def __init__(self, parent):
  369. QScrollArea.__init__(self, parent)
  370. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
  371. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  372. self.keyboard = PixmapKeyboard(self)
  373. self.keyboard.setOctaves(10)
  374. self.setWidget(self.keyboard)
  375. self.setEnabled(False)
  376. self.setFixedHeight(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2)
  377. QTimer.singleShot(0, self.slot_initScrollbarValue)
  378. def setEnabled(self, yesNo):
  379. self.keyboard.setEnabled(yesNo)
  380. QScrollArea.setEnabled(self, yesNo)
  381. @pyqtSlot()
  382. def slot_initScrollbarValue(self):
  383. self.horizontalScrollBar().setValue(self.horizontalScrollBar().maximum()/2)
  384. # ------------------------------------------------------------------------------------------------------------
  385. # Main Testing
  386. if __name__ == '__main__':
  387. import sys
  388. from PyQt4.QtGui import QApplication
  389. import resources_rc
  390. app = QApplication(sys.argv)
  391. gui = PixmapKeyboard(None)
  392. gui.setMode(gui.VERTICAL)
  393. gui.setEnabled(True)
  394. gui.show()
  395. sys.exit(app.exec_())