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.

660 lines
19KB

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