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.

655 lines
19KB

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