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.

448 lines
15KB

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