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.

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