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.

625 lines
18KB

  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. kPcKeysLayouts = {
  128. 'qwerty': kPcKeys_qwerty,
  129. 'qwertz': kPcKeys_qwertz,
  130. }
  131. kBlackNotes = (1, 3, 6, 8, 10)
  132. # ------------------------------------------------------------------------------------------------------------
  133. # MIDI Keyboard, using a pixmap for painting
  134. class PixmapKeyboard(QWidget):
  135. # signals
  136. noteOn = pyqtSignal(int)
  137. noteOff = pyqtSignal(int)
  138. notesOn = pyqtSignal()
  139. notesOff = pyqtSignal()
  140. def __init__(self, parent):
  141. QWidget.__init__(self, parent)
  142. self.fEnabledKeys = []
  143. self.fLastMouseNote = -1
  144. self.fStartOctave = 0
  145. self.fPcKeybOffset = 2
  146. self.fInitalizing = True
  147. self.fFont = self.font()
  148. self.fFont.setFamily("Monospace")
  149. self.fFont.setPixelSize(12)
  150. self.fFont.setBold(True)
  151. self.fPixmapNormal = QPixmap(":/bitmaps/kbd_normal.png")
  152. self.fPixmapDown = QPixmap(":/bitmaps/kbd_down-blue.png")
  153. self.fHighlightColor = "Blue"
  154. self.fkPcKeyLayout = "qwerty"
  155. self.fkPcKeys = kPcKeysLayouts["qwerty"]
  156. self.fKey2RectMap = kMidiKey2RectMapHorizontal
  157. self.fWidth = self.fPixmapNormal.width()
  158. self.fHeight = self.fPixmapNormal.height()
  159. self.setCursor(Qt.PointingHandCursor)
  160. self.setStartOctave(0)
  161. self.setOctaves(6)
  162. self.loadSettings()
  163. self.fInitalizing = False
  164. def saveSettings(self):
  165. if self.fInitalizing:
  166. return
  167. settings = QSettings("falkTX", "CarlaKeyboard")
  168. settings.setValue("PcKeyboardLayout", self.fkPcKeyLayout)
  169. settings.setValue("PcKeyboardOffset", self.fPcKeybOffset)
  170. settings.setValue("HighlightColor", self.fHighlightColor)
  171. del settings
  172. def loadSettings(self):
  173. settings = QSettings("falkTX", "CarlaKeyboard")
  174. self.setPcKeyboardLayout(settings.value("PcKeyboardLayout", "qwerty", type=str))
  175. self.setPcKeyboardOffset(settings.value("PcKeyboardOffset", 2, type=int))
  176. self.setColor(settings.value("HighlightColor", "Blue", type=str))
  177. del settings
  178. def allNotesOff(self, sendSignal=True):
  179. self.fEnabledKeys = []
  180. if sendSignal:
  181. self.notesOff.emit()
  182. self.update()
  183. def sendNoteOn(self, note, sendSignal=True):
  184. if 0 <= note <= 127 and note not in self.fEnabledKeys:
  185. self.fEnabledKeys.append(note)
  186. if sendSignal:
  187. self.noteOn.emit(note)
  188. self.update()
  189. if len(self.fEnabledKeys) == 1:
  190. self.notesOn.emit()
  191. def sendNoteOff(self, note, sendSignal=True):
  192. if 0 <= note <= 127 and note in self.fEnabledKeys:
  193. self.fEnabledKeys.remove(note)
  194. if sendSignal:
  195. self.noteOff.emit(note)
  196. self.update()
  197. if len(self.fEnabledKeys) == 0:
  198. self.notesOff.emit()
  199. def setColor(self, color):
  200. if color not in ("Blue", "Green", "Orange", "Red"):
  201. return
  202. if self.fHighlightColor == color:
  203. return
  204. self.fHighlightColor = color
  205. self.fPixmapDown.load(":/bitmaps/kbd_down-{}.png".format(color.lower()))
  206. self.saveSettings()
  207. def setPcKeyboardLayout(self, layout):
  208. if layout not in kPcKeysLayouts.keys():
  209. return
  210. if self.fkPcKeyLayout == layout:
  211. return
  212. self.fkPcKeyLayout = layout
  213. self.fkPcKeys = kPcKeysLayouts[layout]
  214. self.saveSettings()
  215. def setPcKeyboardOffset(self, offset):
  216. if offset < 0:
  217. offset = 0
  218. elif offset > 9:
  219. offset = 9
  220. if self.fPcKeybOffset == offset:
  221. return
  222. self.fPcKeybOffset = offset
  223. self.saveSettings()
  224. def setOctaves(self, octaves):
  225. if octaves < 1:
  226. octaves = 1
  227. elif octaves > 10:
  228. octaves = 10
  229. self.fOctaves = octaves
  230. self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight)
  231. self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight)
  232. def setStartOctave(self, octave):
  233. if octave < 0:
  234. octave = 0
  235. elif octave > 9:
  236. octave = 9
  237. if self.fStartOctave == octave:
  238. return
  239. self.fStartOctave = octave
  240. self.update()
  241. def handleMousePos(self, pos):
  242. if pos.x() < 0 or pos.x() > self.fOctaves * self.fWidth:
  243. return
  244. octave = int(pos.x() / self.fWidth)
  245. keyPos = QPointF(pos.x() % self.fWidth, pos.y())
  246. if self.fKey2RectMap[1].contains(keyPos): # C#
  247. note = 1
  248. elif self.fKey2RectMap[ 3].contains(keyPos): # D#
  249. note = 3
  250. elif self.fKey2RectMap[ 6].contains(keyPos): # F#
  251. note = 6
  252. elif self.fKey2RectMap[ 8].contains(keyPos): # G#
  253. note = 8
  254. elif self.fKey2RectMap[10].contains(keyPos): # A#
  255. note = 10
  256. elif self.fKey2RectMap[ 0].contains(keyPos): # C
  257. note = 0
  258. elif self.fKey2RectMap[ 2].contains(keyPos): # D
  259. note = 2
  260. elif self.fKey2RectMap[ 4].contains(keyPos): # E
  261. note = 4
  262. elif self.fKey2RectMap[ 5].contains(keyPos): # F
  263. note = 5
  264. elif self.fKey2RectMap[ 7].contains(keyPos): # G
  265. note = 7
  266. elif self.fKey2RectMap[ 9].contains(keyPos): # A
  267. note = 9
  268. elif self.fKey2RectMap[11].contains(keyPos): # B
  269. note = 11
  270. else:
  271. note = -1
  272. if note != -1:
  273. note += (self.fStartOctave + octave) * 12
  274. if self.fLastMouseNote != note:
  275. self.sendNoteOff(self.fLastMouseNote)
  276. self.sendNoteOn(note)
  277. elif self.fLastMouseNote != -1:
  278. self.sendNoteOff(self.fLastMouseNote)
  279. self.fLastMouseNote = note
  280. def showOptions(self, event):
  281. event.accept()
  282. menu = QMenu()
  283. menu.addAction("Note: restart carla to apply globally").setEnabled(False)
  284. menu.addSeparator()
  285. menuColor = QMenu("Highlight color", menu)
  286. actColorBlue = menuColor.addAction("Blue")
  287. actColorGreen = menuColor.addAction("Green")
  288. actColorOrange = menuColor.addAction("Orange")
  289. actColorRed = menuColor.addAction("Red")
  290. actColors = (actColorBlue, actColorGreen, actColorOrange, actColorRed)
  291. for act in actColors:
  292. act.setCheckable(True)
  293. if self.fHighlightColor == act.text():
  294. act.setChecked(True)
  295. menuLayout = QMenu("PC Keyboard layout", menu)
  296. actLayout_qwerty = menuLayout.addAction("qwerty")
  297. actLayout_qwertz = menuLayout.addAction("qwertz")
  298. actLayouts = (actLayout_qwerty, actLayout_qwertz)
  299. for act in actLayouts:
  300. act.setCheckable(True)
  301. if self.fkPcKeyLayout == act.text():
  302. act.setChecked(True)
  303. menu.addMenu(menuColor)
  304. menu.addMenu(menuLayout)
  305. menu.addSeparator()
  306. actOctaveUp = menu.addAction("PC Keyboard octave up")
  307. actOctaveDown = menu.addAction("PC Keyboard octave down")
  308. if self.fPcKeybOffset == 0:
  309. actOctaveDown.setEnabled(False)
  310. actSelected = menu.exec_(event.screenPos().toPoint())
  311. if not actSelected:
  312. return
  313. if actSelected in actColors:
  314. return self.setColor(actSelected.text())
  315. if actSelected in actLayouts:
  316. return self.setPcKeyboardLayout(actSelected.text())
  317. if actSelected == actOctaveUp:
  318. return self.setPcKeyboardOffset(self.fPcKeybOffset + 1)
  319. if actSelected == actOctaveDown:
  320. return self.setPcKeyboardOffset(self.fPcKeybOffset - 1)
  321. def minimumSizeHint(self):
  322. return QSize(self.fWidth, self.fHeight)
  323. def sizeHint(self):
  324. return QSize(self.fWidth * self.fOctaves, self.fHeight)
  325. def keyPressEvent(self, event):
  326. if not event.isAutoRepeat():
  327. try:
  328. qKey = str(event.key())
  329. index = self.fkPcKeys.index(qKey)
  330. except:
  331. pass
  332. else:
  333. self.sendNoteOn(index+(self.fPcKeybOffset*12))
  334. QWidget.keyPressEvent(self, event)
  335. def keyReleaseEvent(self, event):
  336. if not event.isAutoRepeat():
  337. try:
  338. qKey = str(event.key())
  339. index = self.fkPcKeys.index(qKey)
  340. except:
  341. pass
  342. else:
  343. self.sendNoteOff(index+(self.fPcKeybOffset*12))
  344. QWidget.keyReleaseEvent(self, event)
  345. def mousePressEvent(self, event):
  346. if event.button() == Qt.RightButton:
  347. self.showOptions(event)
  348. else:
  349. self.fLastMouseNote = -1
  350. self.handleMousePos(event.pos())
  351. self.setFocus()
  352. QWidget.mousePressEvent(self, event)
  353. def mouseMoveEvent(self, event):
  354. if event.button() != Qt.RightButton:
  355. self.handleMousePos(event.pos())
  356. QWidget.mouseMoveEvent(self, event)
  357. def mouseReleaseEvent(self, event):
  358. if self.fLastMouseNote != -1:
  359. self.sendNoteOff(self.fLastMouseNote)
  360. self.fLastMouseNote = -1
  361. QWidget.mouseReleaseEvent(self, event)
  362. def paintEvent(self, event):
  363. painter = QPainter(self)
  364. event.accept()
  365. # -------------------------------------------------------------
  366. # Paint clean keys (as background)
  367. for octave in range(self.fOctaves):
  368. target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight)
  369. source = QRectF(0, 0, self.fWidth, self.fHeight)
  370. painter.drawPixmap(target, self.fPixmapNormal, source)
  371. if not self.isEnabled():
  372. painter.setBrush(QColor(0, 0, 0, 150))
  373. painter.setPen(QColor(0, 0, 0, 150))
  374. painter.drawRect(0, 0, self.width(), self.height())
  375. return
  376. # -------------------------------------------------------------
  377. # Paint (white) pressed keys
  378. paintedWhite = False
  379. for note in self.fEnabledKeys:
  380. pos = self._getRectFromMidiNote(note)
  381. if self._isNoteBlack(note):
  382. continue
  383. if note < 12:
  384. octave = 0
  385. elif note < 24:
  386. octave = 1
  387. elif note < 36:
  388. octave = 2
  389. elif note < 48:
  390. octave = 3
  391. elif note < 60:
  392. octave = 4
  393. elif note < 72:
  394. octave = 5
  395. elif note < 84:
  396. octave = 6
  397. elif note < 96:
  398. octave = 7
  399. elif note < 108:
  400. octave = 8
  401. elif note < 120:
  402. octave = 9
  403. elif note < 132:
  404. octave = 10
  405. else:
  406. # cannot paint this note
  407. continue
  408. octave -= self.fStartOctave
  409. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  410. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  411. paintedWhite = True
  412. painter.drawPixmap(target, self.fPixmapDown, source)
  413. # -------------------------------------------------------------
  414. # Clear white keys border
  415. if paintedWhite:
  416. for octave in range(self.fOctaves):
  417. for note in kBlackNotes:
  418. pos = self._getRectFromMidiNote(note)
  419. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  420. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  421. painter.drawPixmap(target, self.fPixmapNormal, source)
  422. # -------------------------------------------------------------
  423. # Paint (black) pressed keys
  424. for note in self.fEnabledKeys:
  425. pos = self._getRectFromMidiNote(note)
  426. if not self._isNoteBlack(note):
  427. continue
  428. if note < 12:
  429. octave = 0
  430. elif note < 24:
  431. octave = 1
  432. elif note < 36:
  433. octave = 2
  434. elif note < 48:
  435. octave = 3
  436. elif note < 60:
  437. octave = 4
  438. elif note < 72:
  439. octave = 5
  440. elif note < 84:
  441. octave = 6
  442. elif note < 96:
  443. octave = 7
  444. elif note < 108:
  445. octave = 8
  446. elif note < 120:
  447. octave = 9
  448. elif note < 132:
  449. octave = 10
  450. else:
  451. # cannot paint this note
  452. continue
  453. octave -= self.fStartOctave
  454. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  455. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  456. painter.drawPixmap(target, self.fPixmapDown, source)
  457. # Paint C-number note info
  458. painter.setFont(self.fFont)
  459. painter.setPen(Qt.black)
  460. for i in range(self.fOctaves):
  461. octave = self.fStartOctave + i - 1
  462. painter.drawText(i * 168 + (4 if octave == -1 else 3),
  463. 35, 20, 20,
  464. Qt.AlignCenter,
  465. "C{}".format(octave))
  466. def _isNoteBlack(self, note):
  467. baseNote = note % 12
  468. return bool(baseNote in kBlackNotes)
  469. def _getRectFromMidiNote(self, note):
  470. baseNote = note % 12
  471. return self.fKey2RectMap[baseNote]
  472. # ------------------------------------------------------------------------------------------------------------
  473. # Horizontal scroll area for keyboard
  474. class PixmapKeyboardHArea(QScrollArea):
  475. def __init__(self, parent):
  476. QScrollArea.__init__(self, parent)
  477. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
  478. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  479. self.keyboard = PixmapKeyboard(self)
  480. self.keyboard.setOctaves(10)
  481. self.setWidget(self.keyboard)
  482. self.setEnabled(False)
  483. self.setFixedHeight(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2)
  484. QTimer.singleShot(0, self.slot_initScrollbarValue)
  485. # FIXME use change event
  486. def setEnabled(self, yesNo):
  487. self.keyboard.setEnabled(yesNo)
  488. QScrollArea.setEnabled(self, yesNo)
  489. @pyqtSlot()
  490. def slot_initScrollbarValue(self):
  491. self.horizontalScrollBar().setValue(self.horizontalScrollBar().maximum()/2)
  492. # ------------------------------------------------------------------------------------------------------------
  493. # Main Testing
  494. if __name__ == '__main__':
  495. import sys
  496. from PyQt5.QtWidgets import QApplication
  497. import resources_rc
  498. app = QApplication(sys.argv)
  499. gui = PixmapKeyboard(None)
  500. gui.setEnabled(True)
  501. gui.show()
  502. sys.exit(app.exec_())