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.

643 lines
19KB

  1. #!/usr/bin/env python3
  2. # SPDX-FileCopyrightText: 2011-2025 Filipe Coelho <falktx@falktx.com>
  3. # SPDX-License-Identifier: GPL-2.0-or-later
  4. # ---------------------------------------------------------------------------------------------------------------------
  5. # Imports (Global)
  6. from qt_compat import qt_config
  7. if qt_config == 5:
  8. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QTimer, QSize
  9. from PyQt5.QtGui import QColor, QPainter, QPixmap
  10. from PyQt5.QtWidgets import QActionGroup, QMenu, QScrollArea, QWidget
  11. elif qt_config == 6:
  12. from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QTimer, QSize
  13. from PyQt6.QtGui import QActionGroup, QColor, QPainter, QPixmap
  14. from PyQt6.QtWidgets import QMenu, QScrollArea, QWidget
  15. # ---------------------------------------------------------------------------------------------------------------------
  16. # Imports (Carla)
  17. from utils import QSafeSettings
  18. # ---------------------------------------------------------------------------------------------------------------------
  19. kMidiKey2RectMapHorizontal = [
  20. QRectF(0, 0, 24, 57), # C
  21. QRectF(14, 0, 15, 33), # C#
  22. QRectF(24, 0, 24, 57), # D
  23. QRectF(42, 0, 15, 33), # D#
  24. QRectF(48, 0, 24, 57), # E
  25. QRectF(72, 0, 24, 57), # F
  26. QRectF(84, 0, 15, 33), # F#
  27. QRectF(96, 0, 24, 57), # G
  28. QRectF(112, 0, 15, 33), # G#
  29. QRectF(120, 0, 24, 57), # A
  30. QRectF(140, 0, 15, 33), # A#
  31. QRectF(144, 0, 24, 57), # B
  32. ]
  33. kMidiKey2RectMapVertical = [
  34. QRectF(0, 144, 57, 24), # C
  35. QRectF(0, 139, 33, 15), # C#
  36. QRectF(0, 120, 57, 24), # D
  37. QRectF(0, 111, 33, 15), # D#
  38. QRectF(0, 96, 57, 24), # E
  39. QRectF(0, 72, 57, 24), # F
  40. QRectF(0, 69, 33, 15), # F#
  41. QRectF(0, 48, 57, 24), # G
  42. QRectF(0, 41, 33, 15), # G#
  43. QRectF(0, 24, 57, 24), # A
  44. QRectF(0, 13, 33, 15), # A#
  45. QRectF(0, 0, 57, 24), # B
  46. ]
  47. kPcKeys_qwerty = [
  48. # 1st octave
  49. str(Qt.Key_Z),
  50. str(Qt.Key_S),
  51. str(Qt.Key_X),
  52. str(Qt.Key_D),
  53. str(Qt.Key_C),
  54. str(Qt.Key_V),
  55. str(Qt.Key_G),
  56. str(Qt.Key_B),
  57. str(Qt.Key_H),
  58. str(Qt.Key_N),
  59. str(Qt.Key_J),
  60. str(Qt.Key_M),
  61. # 2nd octave
  62. str(Qt.Key_Q),
  63. str(Qt.Key_2),
  64. str(Qt.Key_W),
  65. str(Qt.Key_3),
  66. str(Qt.Key_E),
  67. str(Qt.Key_R),
  68. str(Qt.Key_5),
  69. str(Qt.Key_T),
  70. str(Qt.Key_6),
  71. str(Qt.Key_Y),
  72. str(Qt.Key_7),
  73. str(Qt.Key_U),
  74. # 3rd octave
  75. str(Qt.Key_I),
  76. str(Qt.Key_9),
  77. str(Qt.Key_O),
  78. str(Qt.Key_0),
  79. str(Qt.Key_P),
  80. ]
  81. kPcKeys_qwertz = [
  82. # 1st octave
  83. str(Qt.Key_Y),
  84. str(Qt.Key_S),
  85. str(Qt.Key_X),
  86. str(Qt.Key_D),
  87. str(Qt.Key_C),
  88. str(Qt.Key_V),
  89. str(Qt.Key_G),
  90. str(Qt.Key_B),
  91. str(Qt.Key_H),
  92. str(Qt.Key_N),
  93. str(Qt.Key_J),
  94. str(Qt.Key_M),
  95. # 2nd octave
  96. str(Qt.Key_Q),
  97. str(Qt.Key_2),
  98. str(Qt.Key_W),
  99. str(Qt.Key_3),
  100. str(Qt.Key_E),
  101. str(Qt.Key_R),
  102. str(Qt.Key_5),
  103. str(Qt.Key_T),
  104. str(Qt.Key_6),
  105. str(Qt.Key_Z),
  106. str(Qt.Key_7),
  107. str(Qt.Key_U),
  108. # 3rd octave
  109. str(Qt.Key_I),
  110. str(Qt.Key_9),
  111. str(Qt.Key_O),
  112. str(Qt.Key_0),
  113. str(Qt.Key_P),
  114. ]
  115. kPcKeys_azerty = [
  116. # 1st octave
  117. str(Qt.Key_W),
  118. str(Qt.Key_S),
  119. str(Qt.Key_X),
  120. str(Qt.Key_D),
  121. str(Qt.Key_C),
  122. str(Qt.Key_V),
  123. str(Qt.Key_G),
  124. str(Qt.Key_B),
  125. str(Qt.Key_H),
  126. str(Qt.Key_N),
  127. str(Qt.Key_J),
  128. str(Qt.Key_Comma),
  129. # 2nd octave
  130. str(Qt.Key_A),
  131. str(Qt.Key_Eacute),
  132. str(Qt.Key_Z),
  133. str(Qt.Key_QuoteDbl),
  134. str(Qt.Key_E),
  135. str(Qt.Key_R),
  136. str(Qt.Key_ParenLeft),
  137. str(Qt.Key_T),
  138. str(Qt.Key_Minus),
  139. str(Qt.Key_Y),
  140. str(Qt.Key_Egrave),
  141. str(Qt.Key_U),
  142. # 3rd octave
  143. str(Qt.Key_I),
  144. str(Qt.Key_Ccedilla),
  145. str(Qt.Key_O),
  146. str(Qt.Key_Agrave),
  147. str(Qt.Key_P),
  148. ]
  149. kPcKeysLayouts = {
  150. 'qwerty': kPcKeys_qwerty,
  151. 'qwertz': kPcKeys_qwertz,
  152. 'azerty': kPcKeys_azerty,
  153. }
  154. kValidColors = ("Blue", "Green", "Orange", "Red")
  155. kBlackNotes = (1, 3, 6, 8, 10)
  156. # ------------------------------------------------------------------------------------------------------------
  157. def _isNoteBlack(note):
  158. baseNote = note % 12
  159. return bool(baseNote in kBlackNotes)
  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 = QSafeSettings("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 = QSafeSettings("falkTX", "CarlaKeyboard")
  202. self.setPcKeyboardLayout(settings.value("PcKeyboardLayout", self.fkPcKeyLayout, str))
  203. self.setPcKeyboardOffset(settings.value("PcKeyboardOffset", self.fPcKeybOffset, int))
  204. self.setColor(settings.value("HighlightColor", self.fHighlightColor, 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(self.tr("Note: restart carla to apply globally")).setEnabled(False)
  312. menu.addAction(self.tr("Color")).setSeparator(True)
  313. groupColor = QActionGroup(menu)
  314. groupLayout = QActionGroup(menu)
  315. actColors = []
  316. actLayouts = []
  317. menu.addAction(self.tr("Highlight color")).setSeparator(True)
  318. for color in kValidColors:
  319. act = menu.addAction(color)
  320. act.setActionGroup(groupColor)
  321. act.setCheckable(True)
  322. if self.fHighlightColor == color:
  323. act.setChecked(True)
  324. actColors.append(act)
  325. menu.addAction(self.tr("PC Keyboard layout")).setSeparator(True)
  326. for pcKeyLayout in kPcKeysLayouts.keys():
  327. act = menu.addAction(pcKeyLayout)
  328. act.setActionGroup(groupLayout)
  329. act.setCheckable(True)
  330. if self.fkPcKeyLayout == pcKeyLayout:
  331. act.setChecked(True)
  332. actLayouts.append(act)
  333. menu.addAction(self.tr("PC Keyboard base octave (%i)" % self.fPcKeybOffset)).setSeparator(True)
  334. actOctaveUp = menu.addAction(self.tr("Octave up"))
  335. actOctaveDown = menu.addAction(self.tr("Octave down"))
  336. if self.fPcKeybOffset == 0:
  337. actOctaveDown.setEnabled(False)
  338. if qt_config == 5:
  339. actSelected = menu.exec_(event.screenPos().toPoint())
  340. else:
  341. actSelected = menu.exec_(event.globalPosition().toPoint())
  342. if not actSelected:
  343. return
  344. if actSelected in actColors:
  345. return self.setColor(actSelected.text())
  346. if actSelected in actLayouts:
  347. return self.setPcKeyboardLayout(actSelected.text())
  348. if actSelected == actOctaveUp:
  349. return self.setPcKeyboardOffset(self.fPcKeybOffset + 1)
  350. if actSelected == actOctaveDown:
  351. return self.setPcKeyboardOffset(self.fPcKeybOffset - 1)
  352. def minimumSizeHint(self):
  353. return QSize(self.fWidth, self.fHeight)
  354. def sizeHint(self):
  355. return QSize(self.fWidth * self.fOctaves, self.fHeight)
  356. def keyPressEvent(self, event):
  357. if not event.isAutoRepeat():
  358. try:
  359. qKey = str(event.key())
  360. index = self.fkPcKeys.index(qKey)
  361. except:
  362. pass
  363. else:
  364. self.sendNoteOn(index+(self.fPcKeybOffset*12))
  365. QWidget.keyPressEvent(self, event)
  366. def keyReleaseEvent(self, event):
  367. if not event.isAutoRepeat():
  368. try:
  369. qKey = str(event.key())
  370. index = self.fkPcKeys.index(qKey)
  371. except:
  372. pass
  373. else:
  374. self.sendNoteOff(index+(self.fPcKeybOffset*12))
  375. QWidget.keyReleaseEvent(self, event)
  376. def mousePressEvent(self, event):
  377. if event.button() == Qt.RightButton:
  378. self.showOptions(event)
  379. else:
  380. self.fLastMouseNote = -1
  381. self.handleMousePos(event.pos())
  382. self.setFocus()
  383. QWidget.mousePressEvent(self, event)
  384. def mouseMoveEvent(self, event):
  385. if event.button() != Qt.RightButton:
  386. self.handleMousePos(event.pos())
  387. QWidget.mouseMoveEvent(self, event)
  388. def mouseReleaseEvent(self, event):
  389. if self.fLastMouseNote != -1:
  390. self.sendNoteOff(self.fLastMouseNote)
  391. self.fLastMouseNote = -1
  392. QWidget.mouseReleaseEvent(self, event)
  393. def paintEvent(self, event):
  394. painter = QPainter(self)
  395. event.accept()
  396. # -------------------------------------------------------------
  397. # Paint clean keys (as background)
  398. for octave in range(self.fOctaves):
  399. target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight)
  400. source = QRectF(0, 0, self.fWidth, self.fHeight)
  401. painter.drawPixmap(target, self.fPixmapNormal, source)
  402. if not self.isEnabled():
  403. painter.setBrush(QColor(0, 0, 0, 150))
  404. painter.setPen(QColor(0, 0, 0, 150))
  405. painter.drawRect(0, 0, self.width(), self.height())
  406. return
  407. # -------------------------------------------------------------
  408. # Paint (white) pressed keys
  409. paintedWhite = False
  410. for note in self.fEnabledKeys:
  411. pos = self._getRectFromMidiNote(note)
  412. if _isNoteBlack(note):
  413. continue
  414. if note < 12:
  415. octave = 0
  416. elif note < 24:
  417. octave = 1
  418. elif note < 36:
  419. octave = 2
  420. elif note < 48:
  421. octave = 3
  422. elif note < 60:
  423. octave = 4
  424. elif note < 72:
  425. octave = 5
  426. elif note < 84:
  427. octave = 6
  428. elif note < 96:
  429. octave = 7
  430. elif note < 108:
  431. octave = 8
  432. elif note < 120:
  433. octave = 9
  434. elif note < 132:
  435. octave = 10
  436. else:
  437. # cannot paint this note
  438. continue
  439. octave -= self.fStartOctave
  440. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  441. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  442. paintedWhite = True
  443. painter.drawPixmap(target, self.fPixmapDown, source)
  444. # -------------------------------------------------------------
  445. # Clear white keys border
  446. if paintedWhite:
  447. for octave in range(self.fOctaves):
  448. for note in kBlackNotes:
  449. pos = self._getRectFromMidiNote(note)
  450. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  451. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  452. painter.drawPixmap(target, self.fPixmapNormal, source)
  453. # -------------------------------------------------------------
  454. # Paint (black) pressed keys
  455. for note in self.fEnabledKeys:
  456. pos = self._getRectFromMidiNote(note)
  457. if not _isNoteBlack(note):
  458. continue
  459. if note < 12:
  460. octave = 0
  461. elif note < 24:
  462. octave = 1
  463. elif note < 36:
  464. octave = 2
  465. elif note < 48:
  466. octave = 3
  467. elif note < 60:
  468. octave = 4
  469. elif note < 72:
  470. octave = 5
  471. elif note < 84:
  472. octave = 6
  473. elif note < 96:
  474. octave = 7
  475. elif note < 108:
  476. octave = 8
  477. elif note < 120:
  478. octave = 9
  479. elif note < 132:
  480. octave = 10
  481. else:
  482. # cannot paint this note
  483. continue
  484. octave -= self.fStartOctave
  485. target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
  486. source = QRectF(pos.x(), 0, pos.width(), pos.height())
  487. painter.drawPixmap(target, self.fPixmapDown, source)
  488. # Paint C-number note info
  489. painter.setFont(self.fFont)
  490. painter.setPen(Qt.black)
  491. for i in range(self.fOctaves):
  492. octave = self.fStartOctave + i - 1
  493. painter.drawText(i * 168 + (4 if octave == -1 else 3),
  494. 35, 20, 20,
  495. Qt.AlignCenter,
  496. "C{}".format(octave))
  497. def _getRectFromMidiNote(self, note):
  498. baseNote = note % 12
  499. return self.fKey2RectMap[baseNote]
  500. # ---------------------------------------------------------------------------------------------------------------------
  501. # Horizontal scroll area for keyboard
  502. class PixmapKeyboardHArea(QScrollArea):
  503. def __init__(self, parent):
  504. QScrollArea.__init__(self, parent)
  505. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
  506. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  507. self.keyboard = PixmapKeyboard(self)
  508. self.keyboard.setOctaves(10)
  509. self.setWidget(self.keyboard)
  510. self.setEnabled(False)
  511. self.setFixedHeight(int(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2))
  512. QTimer.singleShot(0, self.slot_initScrollbarValue)
  513. # FIXME use change event
  514. def setEnabled(self, yesNo):
  515. self.keyboard.setEnabled(yesNo)
  516. QScrollArea.setEnabled(self, yesNo)
  517. @pyqtSlot()
  518. def slot_initScrollbarValue(self):
  519. self.horizontalScrollBar().setValue(int(self.horizontalScrollBar().maximum()/2))
  520. # ---------------------------------------------------------------------------------------------------------------------