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.

650 lines
18KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Pixmap Keyboard, a custom Qt widget
  4. # Copyright (C) 2011-2019 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, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, QSettings, QSize
  20. from PyQt5.QtGui import QColor, QFont, QPainter, QPixmap
  21. from PyQt5.QtWidgets import QMenu, QScrollArea, QWidget
  22. # ------------------------------------------------------------------------------------------------------------
  23. kMidiKey2RectMapHorizontal = [
  24. QRectF(0, 0, 24, 57), # C
  25. QRectF(14, 0, 15, 33), # C#
  26. QRectF(24, 0, 24, 57), # D
  27. QRectF(42, 0, 15, 33), # D#
  28. QRectF(48, 0, 24, 57), # E
  29. QRectF(72, 0, 24, 57), # F
  30. QRectF(84, 0, 15, 33), # F#
  31. QRectF(96, 0, 24, 57), # G
  32. QRectF(112, 0, 15, 33), # G#
  33. QRectF(120, 0, 24, 57), # A
  34. QRectF(140, 0, 15, 33), # A#
  35. QRectF(144, 0, 24, 57), # B
  36. ]
  37. kMidiKey2RectMapVertical = [
  38. QRectF(0, 144, 57, 24), # C
  39. QRectF(0, 139, 33, 15), # C#
  40. QRectF(0, 120, 57, 24), # D
  41. QRectF(0, 111, 33, 15), # D#
  42. QRectF(0, 96, 57, 24), # E
  43. QRectF(0, 72, 57, 24), # F
  44. QRectF(0, 69, 33, 15), # F#
  45. QRectF(0, 48, 57, 24), # G
  46. QRectF(0, 41, 33, 15), # G#
  47. QRectF(0, 24, 57, 24), # A
  48. QRectF(0, 13, 33, 15), # A#
  49. QRectF(0, 0, 57, 24), # B
  50. ]
  51. kPcKeys_qwerty = [
  52. # 1st octave
  53. str(Qt.Key_Z),
  54. str(Qt.Key_S),
  55. str(Qt.Key_X),
  56. str(Qt.Key_D),
  57. str(Qt.Key_C),
  58. str(Qt.Key_V),
  59. str(Qt.Key_G),
  60. str(Qt.Key_B),
  61. str(Qt.Key_H),
  62. str(Qt.Key_N),
  63. str(Qt.Key_J),
  64. str(Qt.Key_M),
  65. # 2nd octave
  66. str(Qt.Key_Q),
  67. str(Qt.Key_2),
  68. str(Qt.Key_W),
  69. str(Qt.Key_3),
  70. str(Qt.Key_E),
  71. str(Qt.Key_R),
  72. str(Qt.Key_5),
  73. str(Qt.Key_T),
  74. str(Qt.Key_6),
  75. str(Qt.Key_Y),
  76. str(Qt.Key_7),
  77. str(Qt.Key_U),
  78. # 3rd octave
  79. str(Qt.Key_I),
  80. str(Qt.Key_9),
  81. str(Qt.Key_O),
  82. str(Qt.Key_0),
  83. str(Qt.Key_P),
  84. ]
  85. kPcKeys_qwertz = [
  86. # 1st octave
  87. str(Qt.Key_Y),
  88. str(Qt.Key_S),
  89. str(Qt.Key_X),
  90. str(Qt.Key_D),
  91. str(Qt.Key_C),
  92. str(Qt.Key_V),
  93. str(Qt.Key_G),
  94. str(Qt.Key_B),
  95. str(Qt.Key_H),
  96. str(Qt.Key_N),
  97. str(Qt.Key_J),
  98. str(Qt.Key_M),
  99. # 2nd octave
  100. str(Qt.Key_Q),
  101. str(Qt.Key_2),
  102. str(Qt.Key_W),
  103. str(Qt.Key_3),
  104. str(Qt.Key_E),
  105. str(Qt.Key_R),
  106. str(Qt.Key_5),
  107. str(Qt.Key_T),
  108. str(Qt.Key_6),
  109. str(Qt.Key_Z),
  110. str(Qt.Key_7),
  111. str(Qt.Key_U),
  112. # 3rd octave
  113. str(Qt.Key_I),
  114. str(Qt.Key_9),
  115. str(Qt.Key_O),
  116. str(Qt.Key_0),
  117. str(Qt.Key_P),
  118. ]
  119. kPcKeys_azerty = [
  120. # 1st octave
  121. str(Qt.Key_W),
  122. str(Qt.Key_S),
  123. str(Qt.Key_X),
  124. str(Qt.Key_D),
  125. str(Qt.Key_C),
  126. str(Qt.Key_V),
  127. str(Qt.Key_G),
  128. str(Qt.Key_B),
  129. str(Qt.Key_H),
  130. str(Qt.Key_N),
  131. str(Qt.Key_J),
  132. str(Qt.Key_Comma),
  133. # 2nd octave
  134. str(Qt.Key_A),
  135. str(Qt.Key_Eacute),
  136. str(Qt.Key_Z),
  137. str(Qt.Key_QuoteDbl),
  138. str(Qt.Key_E),
  139. str(Qt.Key_R),
  140. str(Qt.Key_ParenLeft),
  141. str(Qt.Key_T),
  142. str(Qt.Key_Minus),
  143. str(Qt.Key_Y),
  144. str(Qt.Key_Egrave),
  145. str(Qt.Key_U),
  146. # 3rd octave
  147. str(Qt.Key_I),
  148. str(Qt.Key_Ccedilla),
  149. str(Qt.Key_O),
  150. str(Qt.Key_Agrave),
  151. str(Qt.Key_P),
  152. ]
  153. kPcKeysLayouts = {
  154. 'qwerty': kPcKeys_qwerty,
  155. 'qwertz': kPcKeys_qwertz,
  156. 'azerty': kPcKeys_azerty,
  157. }
  158. kValidColors = ("Blue", "Green", "Orange", "Red")
  159. kBlackNotes = (1, 3, 6, 8, 10)
  160. # ------------------------------------------------------------------------------------------------------------
  161. # MIDI Keyboard, using a pixmap for painting
  162. class PixmapKeyboard(QWidget):
  163. # signals
  164. noteOn = pyqtSignal(int)
  165. noteOff = pyqtSignal(int)
  166. notesOn = pyqtSignal()
  167. notesOff = pyqtSignal()
  168. def __init__(self, parent):
  169. QWidget.__init__(self, parent)
  170. self.fEnabledKeys = []
  171. self.fLastMouseNote = -1
  172. self.fStartOctave = 0
  173. self.fPcKeybOffset = 2
  174. self.fInitalizing = True
  175. self.fFont = self.font()
  176. self.fFont.setFamily("Monospace")
  177. self.fFont.setPixelSize(12)
  178. self.fFont.setBold(True)
  179. self.fPixmapNormal = QPixmap(":/bitmaps/kbd_normal.png")
  180. self.fPixmapDown = QPixmap(":/bitmaps/kbd_down-blue.png")
  181. self.fHighlightColor = kValidColors[0]
  182. self.fkPcKeyLayout = "qwerty"
  183. self.fkPcKeys = kPcKeysLayouts["qwerty"]
  184. self.fKey2RectMap = kMidiKey2RectMapHorizontal
  185. self.fWidth = self.fPixmapNormal.width()
  186. self.fHeight = self.fPixmapNormal.height()
  187. self.setCursor(Qt.PointingHandCursor)
  188. self.setStartOctave(0)
  189. self.setOctaves(6)
  190. self.loadSettings()
  191. self.fInitalizing = False
  192. def saveSettings(self):
  193. if self.fInitalizing:
  194. return
  195. settings = QSettings("falkTX", "CarlaKeyboard")
  196. settings.setValue("PcKeyboardLayout", self.fkPcKeyLayout)
  197. settings.setValue("PcKeyboardOffset", self.fPcKeybOffset)
  198. settings.setValue("HighlightColor", self.fHighlightColor)
  199. del settings
  200. def loadSettings(self):
  201. settings = QSettings("falkTX", "CarlaKeyboard")
  202. self.setPcKeyboardLayout(settings.value("PcKeyboardLayout", self.fkPcKeyLayout, type=str))
  203. self.setPcKeyboardOffset(settings.value("PcKeyboardOffset", self.fPcKeybOffset, type=int))
  204. self.setColor(settings.value("HighlightColor", self.fHighlightColor, type=str))
  205. del settings
  206. def allNotesOff(self, sendSignal=True):
  207. self.fEnabledKeys = []
  208. if sendSignal:
  209. self.notesOff.emit()
  210. self.update()
  211. def sendNoteOn(self, note, sendSignal=True):
  212. if 0 <= note <= 127 and note not in self.fEnabledKeys:
  213. self.fEnabledKeys.append(note)
  214. if sendSignal:
  215. self.noteOn.emit(note)
  216. self.update()
  217. if len(self.fEnabledKeys) == 1:
  218. self.notesOn.emit()
  219. def sendNoteOff(self, note, sendSignal=True):
  220. if 0 <= note <= 127 and note in self.fEnabledKeys:
  221. self.fEnabledKeys.remove(note)
  222. if sendSignal:
  223. self.noteOff.emit(note)
  224. self.update()
  225. if len(self.fEnabledKeys) == 0:
  226. self.notesOff.emit()
  227. def setColor(self, color):
  228. if color not in kValidColors:
  229. return
  230. if self.fHighlightColor == color:
  231. return
  232. self.fHighlightColor = color
  233. self.fPixmapDown.load(":/bitmaps/kbd_down-{}.png".format(color.lower()))
  234. self.saveSettings()
  235. def setPcKeyboardLayout(self, layout):
  236. if layout not in kPcKeysLayouts.keys():
  237. return
  238. if self.fkPcKeyLayout == layout:
  239. return
  240. self.fkPcKeyLayout = layout
  241. self.fkPcKeys = kPcKeysLayouts[layout]
  242. self.saveSettings()
  243. def setPcKeyboardOffset(self, offset):
  244. if offset < 0:
  245. offset = 0
  246. elif offset > 9:
  247. offset = 9
  248. if self.fPcKeybOffset == offset:
  249. return
  250. self.fPcKeybOffset = offset
  251. self.saveSettings()
  252. def setOctaves(self, octaves):
  253. if octaves < 1:
  254. octaves = 1
  255. elif octaves > 10:
  256. octaves = 10
  257. self.fOctaves = octaves
  258. self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight)
  259. self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight)
  260. def setStartOctave(self, octave):
  261. if octave < 0:
  262. octave = 0
  263. elif octave > 9:
  264. octave = 9
  265. if self.fStartOctave == octave:
  266. return
  267. self.fStartOctave = octave
  268. self.update()
  269. def handleMousePos(self, pos):
  270. if pos.x() < 0 or pos.x() > self.fOctaves * self.fWidth:
  271. return
  272. octave = int(pos.x() / self.fWidth)
  273. keyPos = QPointF(pos.x() % self.fWidth, pos.y())
  274. if self.fKey2RectMap[1].contains(keyPos): # C#
  275. note = 1
  276. elif self.fKey2RectMap[ 3].contains(keyPos): # D#
  277. note = 3
  278. elif self.fKey2RectMap[ 6].contains(keyPos): # F#
  279. note = 6
  280. elif self.fKey2RectMap[ 8].contains(keyPos): # G#
  281. note = 8
  282. elif self.fKey2RectMap[10].contains(keyPos): # A#
  283. note = 10
  284. elif self.fKey2RectMap[ 0].contains(keyPos): # C
  285. note = 0
  286. elif self.fKey2RectMap[ 2].contains(keyPos): # D
  287. note = 2
  288. elif self.fKey2RectMap[ 4].contains(keyPos): # E
  289. note = 4
  290. elif self.fKey2RectMap[ 5].contains(keyPos): # F
  291. note = 5
  292. elif self.fKey2RectMap[ 7].contains(keyPos): # G
  293. note = 7
  294. elif self.fKey2RectMap[ 9].contains(keyPos): # A
  295. note = 9
  296. elif self.fKey2RectMap[11].contains(keyPos): # B
  297. note = 11
  298. else:
  299. note = -1
  300. if note != -1:
  301. note += (self.fStartOctave + octave) * 12
  302. if self.fLastMouseNote != note:
  303. self.sendNoteOff(self.fLastMouseNote)
  304. self.sendNoteOn(note)
  305. elif self.fLastMouseNote != -1:
  306. self.sendNoteOff(self.fLastMouseNote)
  307. self.fLastMouseNote = note
  308. def showOptions(self, event):
  309. event.accept()
  310. menu = QMenu()
  311. menu.addAction("Note: restart carla to apply globally").setEnabled(False)
  312. menu.addSeparator()
  313. menuColor = QMenu("Highlight color", menu)
  314. menuLayout = QMenu("PC Keyboard layout", menu)
  315. actColors = []
  316. actLayouts = []
  317. for color in kValidColors:
  318. act = menuColor.addAction(color)
  319. act.setCheckable(True)
  320. if self.fHighlightColor == color:
  321. act.setChecked(True)
  322. actColors.append(act)
  323. for pcKeyLayout in kPcKeysLayouts.keys():
  324. act = menuLayout.addAction(pcKeyLayout)
  325. act.setCheckable(True)
  326. if self.fkPcKeyLayout == pcKeyLayout:
  327. act.setChecked(True)
  328. actLayouts.append(act)
  329. menu.addMenu(menuColor)
  330. menu.addMenu(menuLayout)
  331. menu.addSeparator()
  332. actOctaveUp = menu.addAction("PC Keyboard octave up")
  333. actOctaveDown = menu.addAction("PC Keyboard octave down")
  334. if self.fPcKeybOffset == 0:
  335. actOctaveDown.setEnabled(False)
  336. actSelected = menu.exec_(event.screenPos().toPoint())
  337. if not actSelected:
  338. return
  339. if actSelected in actColors:
  340. return self.setColor(actSelected.text())
  341. if actSelected in actLayouts:
  342. return self.setPcKeyboardLayout(actSelected.text())
  343. if actSelected == actOctaveUp:
  344. return self.setPcKeyboardOffset(self.fPcKeybOffset + 1)
  345. if actSelected == actOctaveDown:
  346. return self.setPcKeyboardOffset(self.fPcKeybOffset - 1)
  347. def minimumSizeHint(self):
  348. return QSize(self.fWidth, self.fHeight)
  349. def sizeHint(self):
  350. return QSize(self.fWidth * self.fOctaves, self.fHeight)
  351. def keyPressEvent(self, event):
  352. if not event.isAutoRepeat():
  353. try:
  354. qKey = str(event.key())
  355. index = self.fkPcKeys.index(qKey)
  356. except:
  357. pass
  358. else:
  359. self.sendNoteOn(index+(self.fPcKeybOffset*12))
  360. QWidget.keyPressEvent(self, event)
  361. def keyReleaseEvent(self, event):
  362. if not event.isAutoRepeat():
  363. try:
  364. qKey = str(event.key())
  365. index = self.fkPcKeys.index(qKey)
  366. except:
  367. pass
  368. else:
  369. self.sendNoteOff(index+(self.fPcKeybOffset*12))
  370. QWidget.keyReleaseEvent(self, event)
  371. def mousePressEvent(self, event):
  372. if event.button() == Qt.RightButton:
  373. self.showOptions(event)
  374. else:
  375. self.fLastMouseNote = -1
  376. self.handleMousePos(event.pos())
  377. self.setFocus()
  378. QWidget.mousePressEvent(self, event)
  379. def mouseMoveEvent(self, event):
  380. if event.button() != Qt.RightButton:
  381. self.handleMousePos(event.pos())
  382. QWidget.mouseMoveEvent(self, event)
  383. def mouseReleaseEvent(self, event):
  384. if self.fLastMouseNote != -1:
  385. self.sendNoteOff(self.fLastMouseNote)
  386. self.fLastMouseNote = -1
  387. QWidget.mouseReleaseEvent(self, event)
  388. def paintEvent(self, event):
  389. painter = QPainter(self)
  390. event.accept()
  391. # -------------------------------------------------------------
  392. # Paint clean keys (as background)
  393. for octave in range(self.fOctaves):
  394. target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight)
  395. source = QRectF(0, 0, self.fWidth, self.fHeight)
  396. painter.drawPixmap(target, self.fPixmapNormal, source)
  397. if not self.isEnabled():
  398. painter.setBrush(QColor(0, 0, 0, 150))
  399. painter.setPen(QColor(0, 0, 0, 150))
  400. painter.drawRect(0, 0, self.width(), self.height())
  401. return
  402. # -------------------------------------------------------------
  403. # Paint (white) pressed keys
  404. paintedWhite = False
  405. for note in self.fEnabledKeys:
  406. pos = self._getRectFromMidiNote(note)
  407. if self._isNoteBlack(note):
  408. continue
  409. if note < 12:
  410. octave = 0
  411. elif note < 24:
  412. octave = 1
  413. elif note < 36:
  414. octave = 2
  415. elif note < 48:
  416. octave = 3
  417. elif note < 60:
  418. octave = 4
  419. elif note < 72:
  420. octave = 5
  421. elif note < 84:
  422. octave = 6
  423. elif note < 96:
  424. octave = 7
  425. elif note < 108:
  426. octave = 8
  427. elif note < 120:
  428. octave = 9
  429. elif note < 132:
  430. octave = 10
  431. else:
  432. # cannot paint this note
  433. continue
  434. octave -= self.fStartOctave
  435. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  436. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  437. paintedWhite = True
  438. painter.drawPixmap(target, self.fPixmapDown, source)
  439. # -------------------------------------------------------------
  440. # Clear white keys border
  441. if paintedWhite:
  442. for octave in range(self.fOctaves):
  443. for note in kBlackNotes:
  444. pos = self._getRectFromMidiNote(note)
  445. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  446. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  447. painter.drawPixmap(target, self.fPixmapNormal, source)
  448. # -------------------------------------------------------------
  449. # Paint (black) pressed keys
  450. for note in self.fEnabledKeys:
  451. pos = self._getRectFromMidiNote(note)
  452. if not self._isNoteBlack(note):
  453. continue
  454. if note < 12:
  455. octave = 0
  456. elif note < 24:
  457. octave = 1
  458. elif note < 36:
  459. octave = 2
  460. elif note < 48:
  461. octave = 3
  462. elif note < 60:
  463. octave = 4
  464. elif note < 72:
  465. octave = 5
  466. elif note < 84:
  467. octave = 6
  468. elif note < 96:
  469. octave = 7
  470. elif note < 108:
  471. octave = 8
  472. elif note < 120:
  473. octave = 9
  474. elif note < 132:
  475. octave = 10
  476. else:
  477. # cannot paint this note
  478. continue
  479. octave -= self.fStartOctave
  480. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  481. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  482. painter.drawPixmap(target, self.fPixmapDown, source)
  483. # Paint C-number note info
  484. painter.setFont(self.fFont)
  485. painter.setPen(Qt.black)
  486. for i in range(self.fOctaves):
  487. octave = self.fStartOctave + i - 1
  488. painter.drawText(i * 168 + (4 if octave == -1 else 3),
  489. 35, 20, 20,
  490. Qt.AlignCenter,
  491. "C{}".format(octave))
  492. def _isNoteBlack(self, note):
  493. baseNote = note % 12
  494. return bool(baseNote in kBlackNotes)
  495. def _getRectFromMidiNote(self, note):
  496. baseNote = note % 12
  497. return self.fKey2RectMap[baseNote]
  498. # ------------------------------------------------------------------------------------------------------------
  499. # Horizontal scroll area for keyboard
  500. class PixmapKeyboardHArea(QScrollArea):
  501. def __init__(self, parent):
  502. QScrollArea.__init__(self, parent)
  503. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
  504. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  505. self.keyboard = PixmapKeyboard(self)
  506. self.keyboard.setOctaves(10)
  507. self.setWidget(self.keyboard)
  508. self.setEnabled(False)
  509. self.setFixedHeight(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2)
  510. QTimer.singleShot(0, self.slot_initScrollbarValue)
  511. # FIXME use change event
  512. def setEnabled(self, yesNo):
  513. self.keyboard.setEnabled(yesNo)
  514. QScrollArea.setEnabled(self, yesNo)
  515. @pyqtSlot()
  516. def slot_initScrollbarValue(self):
  517. self.horizontalScrollBar().setValue(self.horizontalScrollBar().maximum()/2)
  518. # ------------------------------------------------------------------------------------------------------------
  519. # Main Testing
  520. if __name__ == '__main__':
  521. import sys
  522. from PyQt5.QtWidgets import QApplication
  523. import resources_rc
  524. app = QApplication(sys.argv)
  525. gui = PixmapKeyboard(None)
  526. gui.setEnabled(True)
  527. gui.show()
  528. sys.exit(app.exec_())