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.

506 lines
17KB

  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 (Config)
  19. from carla_config import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (Global)
  22. if config_UseQt5:
  23. from PyQt5.QtCore import pyqtSignal, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, QSize
  24. from PyQt5.QtGui import QColor, QFont, QPainter, QPixmap
  25. from PyQt5.QtWidgets import QScrollArea, QWidget
  26. else:
  27. from PyQt4.QtCore import pyqtSignal, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, QSize
  28. from PyQt4.QtGui import QColor, QFont, QPainter, QPixmap, QScrollArea, QWidget
  29. # ------------------------------------------------------------------------------------------------------------
  30. kMidiKey2RectMapHorizontal = {
  31. '0': QRectF(0, 0, 24, 57), # C
  32. '1': QRectF(14, 0, 15, 33), # C#
  33. '2': QRectF(24, 0, 24, 57), # D
  34. '3': QRectF(42, 0, 15, 33), # D#
  35. '4': QRectF(48, 0, 24, 57), # E
  36. '5': QRectF(72, 0, 24, 57), # F
  37. '6': QRectF(84, 0, 15, 33), # F#
  38. '7': QRectF(96, 0, 24, 57), # G
  39. '8': QRectF(112, 0, 15, 33), # G#
  40. '9': QRectF(120, 0, 24, 57), # A
  41. '10': QRectF(140, 0, 15, 33), # A#
  42. '11': QRectF(144, 0, 24, 57) # B
  43. }
  44. kMidiKey2RectMapVertical = {
  45. '11': QRectF(0, 0, 57, 24), # B
  46. '10': QRectF(0, 13, 33, 15), # A#
  47. '9': QRectF(0, 24, 57, 24), # A
  48. '8': QRectF(0, 41, 33, 15), # G#
  49. '7': QRectF(0, 48, 57, 24), # G
  50. '6': QRectF(0, 69, 33, 15), # F#
  51. '5': QRectF(0, 72, 57, 24), # F
  52. '4': QRectF(0, 96, 57, 24), # E
  53. '3': QRectF(0, 111, 33, 15), # D#
  54. '2': QRectF(0, 120, 57, 24), # D
  55. '1': QRectF(0, 139, 33, 15), # C#
  56. '0': QRectF(0, 144, 57, 24) # C
  57. }
  58. kMidiKeyboard2KeyMap = {
  59. # 3th octave
  60. '%i' % Qt.Key_Z: 48,
  61. '%i' % Qt.Key_S: 49,
  62. '%i' % Qt.Key_X: 50,
  63. '%i' % Qt.Key_D: 51,
  64. '%i' % Qt.Key_C: 52,
  65. '%i' % Qt.Key_V: 53,
  66. '%i' % Qt.Key_G: 54,
  67. '%i' % Qt.Key_B: 55,
  68. '%i' % Qt.Key_H: 56,
  69. '%i' % Qt.Key_N: 57,
  70. '%i' % Qt.Key_J: 58,
  71. '%i' % Qt.Key_M: 59,
  72. # 4th octave
  73. '%i' % Qt.Key_Q: 60,
  74. '%i' % Qt.Key_2: 61,
  75. '%i' % Qt.Key_W: 62,
  76. '%i' % Qt.Key_3: 63,
  77. '%i' % Qt.Key_E: 64,
  78. '%i' % Qt.Key_R: 65,
  79. '%i' % Qt.Key_5: 66,
  80. '%i' % Qt.Key_T: 67,
  81. '%i' % Qt.Key_6: 68,
  82. '%i' % Qt.Key_Y: 69,
  83. '%i' % Qt.Key_7: 70,
  84. '%i' % Qt.Key_U: 71,
  85. # 5th octave
  86. '%i' % Qt.Key_I: 72,
  87. '%i' % Qt.Key_9: 73,
  88. '%i' % Qt.Key_O: 74,
  89. '%i' % Qt.Key_0: 75,
  90. '%i' % Qt.Key_P: 76,
  91. }
  92. kBlackNotes = (1, 3, 6, 8, 10)
  93. # ------------------------------------------------------------------------------------------------------------
  94. # MIDI Keyboard, using a pixmap for painting
  95. class PixmapKeyboard(QWidget):
  96. # enum Orientation
  97. HORIZONTAL = 0
  98. VERTICAL = 1
  99. # signals
  100. noteOn = pyqtSignal(int)
  101. noteOff = pyqtSignal(int)
  102. notesOn = pyqtSignal()
  103. notesOff = pyqtSignal()
  104. def __init__(self, parent):
  105. QWidget.__init__(self, parent)
  106. self.fOctaves = 6
  107. self.fLastMouseNote = -1
  108. self.fEnabledKeys = []
  109. self.fFont = self.font()
  110. self.fFont.setFamily("Monospace")
  111. self.fFont.setPixelSize(12)
  112. self.fFont.setBold(True)
  113. self.fPixmap = QPixmap("")
  114. self.setCursor(Qt.PointingHandCursor)
  115. self.setMode(self.HORIZONTAL)
  116. def allNotesOff(self, sendSignal=True):
  117. self.fEnabledKeys = []
  118. if sendSignal:
  119. self.notesOff.emit()
  120. self.update()
  121. def sendNoteOn(self, note, sendSignal=True):
  122. if 0 <= note <= 127 and note not in self.fEnabledKeys:
  123. self.fEnabledKeys.append(note)
  124. if sendSignal:
  125. self.noteOn.emit(note)
  126. self.update()
  127. if len(self.fEnabledKeys) == 1:
  128. self.notesOn.emit()
  129. def sendNoteOff(self, note, sendSignal=True):
  130. if 0 <= note <= 127 and note in self.fEnabledKeys:
  131. self.fEnabledKeys.remove(note)
  132. if sendSignal:
  133. self.noteOff.emit(note)
  134. self.update()
  135. if len(self.fEnabledKeys) == 0:
  136. self.notesOff.emit()
  137. def setMode(self, mode):
  138. if mode == self.HORIZONTAL:
  139. self.fMidiMap = kMidiKey2RectMapHorizontal
  140. self.fPixmap.load(":/bitmaps/kbd_h_dark.png")
  141. self.fPixmapMode = self.HORIZONTAL
  142. self.fWidth = self.fPixmap.width()
  143. self.fHeight = self.fPixmap.height() / 2
  144. elif mode == self.VERTICAL:
  145. self.fMidiMap = kMidiKey2RectMapVertical
  146. self.fPixmap.load(":/bitmaps/kbd_v_dark.png")
  147. self.fPixmapMode = self.VERTICAL
  148. self.fWidth = self.fPixmap.width() / 2
  149. self.fHeight = self.fPixmap.height()
  150. else:
  151. qCritical("PixmapKeyboard::setMode(%i) - invalid mode" % mode)
  152. return self.setMode(self.HORIZONTAL)
  153. self.setOctaves(self.fOctaves)
  154. def setOctaves(self, octaves):
  155. if octaves < 1:
  156. octaves = 1
  157. elif octaves > 10:
  158. octaves = 10
  159. self.fOctaves = octaves
  160. if self.fPixmapMode == self.HORIZONTAL:
  161. self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight)
  162. self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight)
  163. elif self.fPixmapMode == self.VERTICAL:
  164. self.setMinimumSize(self.fWidth, self.fHeight * self.fOctaves)
  165. self.setMaximumSize(self.fWidth, self.fHeight * self.fOctaves)
  166. self.update()
  167. def handleMousePos(self, pos):
  168. if self.fPixmapMode == self.HORIZONTAL:
  169. if pos.x() < 0 or pos.x() > self.fOctaves * self.fWidth:
  170. return
  171. octave = int(pos.x() / self.fWidth)
  172. keyPos = QPointF(pos.x() % self.fWidth, pos.y())
  173. elif self.fPixmapMode == self.VERTICAL:
  174. if pos.y() < 0 or pos.y() > self.fOctaves * self.fHeight:
  175. return
  176. octave = int(self.fOctaves - pos.y() / self.fHeight)
  177. keyPos = QPointF(pos.x(), (pos.y()-1) % 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. if not self.isEnabled():
  263. painter.setBrush(QColor(0, 0, 0, 150))
  264. painter.setPen(QColor(0, 0, 0, 150))
  265. painter.drawRect(0, 0, self.width(), self.height())
  266. return
  267. # -------------------------------------------------------------
  268. # Paint (white) pressed keys
  269. paintedWhite = False
  270. for note in self.fEnabledKeys:
  271. pos = self._getRectFromMidiNote(note)
  272. if self._isNoteBlack(note):
  273. continue
  274. if note < 12:
  275. octave = 0
  276. elif note < 24:
  277. octave = 1
  278. elif note < 36:
  279. octave = 2
  280. elif note < 48:
  281. octave = 3
  282. elif note < 60:
  283. octave = 4
  284. elif note < 72:
  285. octave = 5
  286. elif note < 84:
  287. octave = 6
  288. elif note < 96:
  289. octave = 7
  290. elif note < 108:
  291. octave = 8
  292. elif note < 120:
  293. octave = 9
  294. elif note < 132:
  295. octave = 10
  296. else:
  297. # cannot paint this note
  298. continue
  299. if self.fPixmapMode == self.VERTICAL:
  300. octave = self.fOctaves - octave - 1
  301. if self.fPixmapMode == self.HORIZONTAL:
  302. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  303. source = QRectF(pos.x(), self.fHeight, pos.width(), pos.height())
  304. elif self.fPixmapMode == self.VERTICAL:
  305. target = QRectF(pos.x(), pos.y() + (self.fHeight * octave), pos.width(), pos.height())
  306. source = QRectF(self.fWidth, pos.y(), pos.width(), pos.height())
  307. else:
  308. return
  309. paintedWhite = True
  310. painter.drawPixmap(target, self.fPixmap, source)
  311. # -------------------------------------------------------------
  312. # Clear white keys border
  313. if paintedWhite:
  314. for octave in range(self.fOctaves):
  315. for note in kBlackNotes:
  316. pos = self._getRectFromMidiNote(note)
  317. if self.fPixmapMode == self.HORIZONTAL:
  318. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  319. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  320. elif self.fPixmapMode == self.VERTICAL:
  321. target = QRectF(pos.x(), pos.y() + (self.fHeight * octave), pos.width(), pos.height())
  322. source = QRectF(0, pos.y(), pos.width(), pos.height())
  323. else:
  324. return
  325. painter.drawPixmap(target, self.fPixmap, source)
  326. # -------------------------------------------------------------
  327. # Paint (black) pressed keys
  328. for note in self.fEnabledKeys:
  329. pos = self._getRectFromMidiNote(note)
  330. if not self._isNoteBlack(note):
  331. continue
  332. if note < 12:
  333. octave = 0
  334. elif note < 24:
  335. octave = 1
  336. elif note < 36:
  337. octave = 2
  338. elif note < 48:
  339. octave = 3
  340. elif note < 60:
  341. octave = 4
  342. elif note < 72:
  343. octave = 5
  344. elif note < 84:
  345. octave = 6
  346. elif note < 96:
  347. octave = 7
  348. elif note < 108:
  349. octave = 8
  350. elif note < 120:
  351. octave = 9
  352. elif note < 132:
  353. octave = 10
  354. else:
  355. # cannot paint this note
  356. continue
  357. if self.fPixmapMode == self.VERTICAL:
  358. octave = self.fOctaves - octave - 1
  359. if self.fPixmapMode == self.HORIZONTAL:
  360. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  361. source = QRectF(pos.x(), self.fHeight, pos.width(), pos.height())
  362. elif self.fPixmapMode == self.VERTICAL:
  363. target = QRectF(pos.x(), pos.y() + (self.fHeight * octave), pos.width(), pos.height())
  364. source = QRectF(self.fWidth, pos.y(), pos.width(), pos.height())
  365. else:
  366. return
  367. painter.drawPixmap(target, self.fPixmap, source)
  368. # Paint C-number note info
  369. painter.setFont(self.fFont)
  370. painter.setPen(Qt.black)
  371. for i in range(self.fOctaves):
  372. if self.fPixmapMode == self.HORIZONTAL:
  373. painter.drawText(i * 168 + (4 if i == 0 else 3), 35, 20, 20, Qt.AlignCenter, "C%i" % (i-1))
  374. elif self.fPixmapMode == self.VERTICAL:
  375. painter.drawText(33, (self.fOctaves * 168) - (i * 168) - 20, 20, 20, Qt.AlignCenter, "C%i" % (i-1))
  376. def _isNoteBlack(self, note):
  377. baseNote = note % 12
  378. return bool(baseNote in kBlackNotes)
  379. def _getRectFromMidiNote(self, note):
  380. baseNote = note % 12
  381. return self.fMidiMap.get(str(baseNote))
  382. # ------------------------------------------------------------------------------------------------------------
  383. # Horizontal scroll area for keyboard
  384. class PixmapKeyboardHArea(QScrollArea):
  385. def __init__(self, parent):
  386. QScrollArea.__init__(self, parent)
  387. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
  388. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  389. self.keyboard = PixmapKeyboard(self)
  390. self.keyboard.setMode(PixmapKeyboard.HORIZONTAL)
  391. self.keyboard.setOctaves(10)
  392. self.setWidget(self.keyboard)
  393. self.setEnabled(False)
  394. self.setFixedHeight(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2)
  395. QTimer.singleShot(0, self.slot_initScrollbarValue)
  396. # FIXME use change event
  397. def setEnabled(self, yesNo):
  398. self.keyboard.setEnabled(yesNo)
  399. QScrollArea.setEnabled(self, yesNo)
  400. @pyqtSlot()
  401. def slot_initScrollbarValue(self):
  402. self.horizontalScrollBar().setValue(self.horizontalScrollBar().maximum()/2)
  403. # ------------------------------------------------------------------------------------------------------------
  404. # Main Testing
  405. if __name__ == '__main__':
  406. import sys
  407. from PyQt5.QtWidgets import QApplication
  408. import resources_rc
  409. app = QApplication(sys.argv)
  410. gui = PixmapKeyboard(None)
  411. gui.setMode(gui.HORIZONTAL)
  412. #gui.setMode(gui.VERTICAL)
  413. gui.setEnabled(True)
  414. gui.show()
  415. sys.exit(app.exec_())