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.

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