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.

1898 lines
73KB

  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 abc import abstractmethod
  7. # ------------------------------------------------------------------------------------------------------------
  8. # Imports (PyQt)
  9. from qt_compat import qt_config
  10. if qt_config == 5:
  11. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray
  12. from PyQt5.QtGui import QCursor, QIcon, QPalette, QPixmap, QFont
  13. from PyQt5.QtWidgets import (
  14. QDialog,
  15. QFileDialog,
  16. QInputDialog,
  17. QMenu,
  18. QMessageBox,
  19. QScrollArea,
  20. QVBoxLayout,
  21. QWidget,
  22. QGraphicsScene,
  23. QGraphicsTextItem,
  24. QGraphicsView,
  25. QTableWidget,
  26. QTableWidgetItem,
  27. QHeaderView,
  28. QLabel,
  29. QSizePolicy,
  30. )
  31. elif qt_config == 6:
  32. from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray
  33. from PyQt6.QtGui import QCursor, QIcon, QPalette, QPixmap, QFont
  34. from PyQt6.QtWidgets import (
  35. QDialog,
  36. QFileDialog,
  37. QInputDialog,
  38. QMenu,
  39. QMessageBox,
  40. QScrollArea,
  41. QVBoxLayout,
  42. QWidget,
  43. QGraphicsScene,
  44. QGraphicsTextItem,
  45. QGraphicsView,
  46. QTableWidget,
  47. QTableWidgetItem,
  48. QHeaderView,
  49. QLabel,
  50. QSizePolicy,
  51. )
  52. # ------------------------------------------------------------------------------------------------------------
  53. # Imports (Custom)
  54. import ui_carla_edit
  55. import ui_carla_parameter
  56. from carla_backend import (
  57. BINARY_NATIVE,
  58. CARLA_VERSION_STRING,
  59. CARLA_OS_MAC,
  60. CARLA_OS_WIN,
  61. PLUGIN_INTERNAL,
  62. PLUGIN_DSSI,
  63. PLUGIN_LV2,
  64. PLUGIN_VST2,
  65. PLUGIN_SF2,
  66. PLUGIN_SFZ,
  67. PLUGIN_CAN_DRYWET,
  68. PLUGIN_CAN_VOLUME,
  69. PLUGIN_CAN_BALANCE,
  70. PLUGIN_CAN_PANNING,
  71. PLUGIN_CAN_FORTH,
  72. PLUGIN_CATEGORY_SYNTH,
  73. PLUGIN_OPTION_FIXED_BUFFERS,
  74. PLUGIN_OPTION_FORCE_STEREO,
  75. PLUGIN_OPTION_MAP_PROGRAM_CHANGES,
  76. PLUGIN_OPTION_USE_CHUNKS,
  77. PLUGIN_OPTION_SEND_CONTROL_CHANGES,
  78. PLUGIN_OPTION_SEND_CHANNEL_PRESSURE,
  79. PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH,
  80. PLUGIN_OPTION_SEND_PITCHBEND,
  81. PLUGIN_OPTION_SEND_ALL_SOUND_OFF,
  82. PLUGIN_OPTION_SEND_PROGRAM_CHANGES,
  83. PLUGIN_OPTION_SKIP_SENDING_NOTES,
  84. PARAMETER_NULL,
  85. PARAMETER_DRYWET,
  86. PARAMETER_VOLUME,
  87. PARAMETER_BALANCE_LEFT,
  88. PARAMETER_BALANCE_RIGHT,
  89. PARAMETER_PANNING,
  90. PARAMETER_FORTH,
  91. PARAMETER_CTRL_CHANNEL,
  92. PARAMETER_IS_ENABLED,
  93. PARAMETER_IS_AUTOMATABLE,
  94. PARAMETER_IS_READ_ONLY,
  95. PARAMETER_USES_SCALEPOINTS,
  96. PARAMETER_USES_CUSTOM_TEXT,
  97. PARAMETER_CAN_BE_CV_CONTROLLED,
  98. parameterHintsText,
  99. PARAMETER_INPUT, PARAMETER_OUTPUT,
  100. CONTROL_INDEX_NONE,
  101. CONTROL_INDEX_MIDI_PITCHBEND,
  102. CONTROL_INDEX_MIDI_LEARN,
  103. CONTROL_INDEX_CV
  104. )
  105. from carla_shared import (
  106. MIDI_CC_LIST, MAX_MIDI_CC_LIST_ITEM,
  107. countDecimalPoints,
  108. strLim,
  109. fontMetricsHorizontalAdvance,
  110. setUpSignals,
  111. gCarla
  112. )
  113. from carla_utils import getPluginTypeAsString
  114. from widgets.collapsablewidget import CollapsibleBox
  115. from widgets.pixmapkeyboard import PixmapKeyboardHArea
  116. from widgets.paramspinbox import CustomInputDialog, ParamSpinBox
  117. from widgets.scalabledial import ScalableDial
  118. # ------------------------------------------------------------------------------------------------------------
  119. # Carla GUI defines
  120. ICON_STATE_ON = 3 # turns on, sets as wait
  121. ICON_STATE_WAIT = 2 # nothing, sets as off
  122. ICON_STATE_OFF = 1 # turns off, sets as null
  123. ICON_STATE_NULL = 0 # nothing
  124. # ------------------------------------------------------------------------------------------------------------
  125. # Plugin Parameter
  126. class PluginParameter(QWidget):
  127. mappedControlChanged = pyqtSignal(int, int)
  128. mappedRangeChanged = pyqtSignal(int, float, float)
  129. midiChannelChanged = pyqtSignal(int, int)
  130. knobVisibilityChanged = pyqtSignal(int, int)
  131. valueChanged = pyqtSignal(int, float)
  132. def __init__(self, parent, host, pInfo, pluginId, tabIndex):
  133. QWidget.__init__(self, parent)
  134. self.host = host
  135. self.ui = ui_carla_parameter.Ui_PluginParameter()
  136. self.ui.setupUi(self)
  137. # -------------------------------------------------------------
  138. # Internal stuff
  139. self.fDecimalPoints = max(2, countDecimalPoints(pInfo['step'], pInfo['stepSmall']))
  140. self.fCanBeInCV = pInfo['hints'] & PARAMETER_CAN_BE_CV_CONTROLLED
  141. self.fMappedCtrl = pInfo['mappedControlIndex']
  142. self.fMappedMinimum = pInfo['mappedMinimum']
  143. self.fMappedMaximum = pInfo['mappedMaximum']
  144. self.fMinimum = pInfo['minimum']
  145. self.fMaximum = pInfo['maximum']
  146. self.fMidiChannel = pInfo['midiChannel']
  147. self.fParameterId = pInfo['index']
  148. self.fPluginId = pluginId
  149. self.fTabIndex = tabIndex
  150. self.fKnobVisible = True
  151. # -------------------------------------------------------------
  152. # Set-up GUI
  153. pType = pInfo['type']
  154. pHints = pInfo['hints']
  155. self.ui.l_name.setText(pInfo['name'])
  156. self.ui.widget.setName(pInfo['name'])
  157. self.ui.widget.setMinimum(pInfo['minimum'])
  158. self.ui.widget.setMaximum(pInfo['maximum'])
  159. self.ui.widget.setDefault(pInfo['default'])
  160. self.ui.widget.setLabel(pInfo['unit'])
  161. self.ui.widget.setStep(pInfo['step'])
  162. self.ui.widget.setStepSmall(pInfo['stepSmall'])
  163. self.ui.widget.setStepLarge(pInfo['stepLarge'])
  164. # NOTE: Issue #1983
  165. self.ui.widget.setScalePoints(pInfo['scalePoints'], bool(pHints & PARAMETER_USES_SCALEPOINTS))
  166. if pInfo['comment']:
  167. self.ui.l_name.setToolTip(pInfo['comment'])
  168. self.ui.widget.setToolTip(pInfo['comment'])
  169. if pType == PARAMETER_INPUT:
  170. if not pHints & PARAMETER_IS_ENABLED:
  171. self.ui.l_name.setEnabled(False)
  172. self.ui.l_status.setEnabled(False)
  173. self.ui.widget.setEnabled(False)
  174. self.ui.widget.setReadOnly(True)
  175. self.ui.tb_options.setEnabled(False)
  176. elif not pHints & PARAMETER_IS_AUTOMATABLE:
  177. self.ui.l_status.setEnabled(False)
  178. self.ui.tb_options.setEnabled(False)
  179. if pHints & PARAMETER_IS_READ_ONLY:
  180. self.ui.l_status.setEnabled(False)
  181. self.ui.widget.setReadOnly(True)
  182. self.ui.tb_options.setEnabled(False)
  183. elif pType == PARAMETER_OUTPUT:
  184. self.ui.widget.setReadOnly(True)
  185. else:
  186. self.ui.l_status.setVisible(False)
  187. self.ui.widget.setVisible(False)
  188. self.ui.tb_options.setVisible(False)
  189. # Only set value after all hints are handled
  190. self.ui.widget.setValue(pInfo['current'])
  191. if pHints & PARAMETER_USES_CUSTOM_TEXT and not host.isPlugin:
  192. self.ui.widget.setTextCallback(self._textCallBack)
  193. self.ui.l_status.setFixedWidth(fontMetricsHorizontalAdvance(self.ui.l_status.fontMetrics(),
  194. self.tr("CC%i Ch%i" % (119,16))))
  195. self.ui.widget.setValueCallback(self._valueCallBack)
  196. self.ui.widget.updateAll()
  197. self.setMappedControlIndex(pInfo['mappedControlIndex'])
  198. self.setMidiChannel(pInfo['midiChannel'])
  199. self.updateStatusLabel()
  200. # -------------------------------------------------------------
  201. # Set-up connections
  202. self.ui.tb_options.clicked.connect(self.slot_optionsCustomMenu)
  203. self.ui.widget.dragStateChanged.connect(self.slot_parameterDragStateChanged)
  204. # -------------------------------------------------------------
  205. def getPluginId(self):
  206. return self.fPluginId
  207. def getTabIndex(self):
  208. return self.fTabIndex
  209. def setPluginId(self, pluginId):
  210. self.fPluginId = pluginId
  211. def setDefault(self, value):
  212. self.ui.widget.setDefault(value)
  213. def setValue(self, value):
  214. self.ui.widget.blockSignals(True)
  215. self.ui.widget.setValue(value)
  216. self.ui.widget.blockSignals(False)
  217. def setMappedControlIndex(self, control):
  218. self.fMappedCtrl = control
  219. self.updateStatusLabel()
  220. def setMappedRange(self, minimum, maximum):
  221. self.fMappedMinimum = minimum
  222. self.fMappedMaximum = maximum
  223. def setMidiChannel(self, channel):
  224. self.fMidiChannel = channel
  225. self.updateStatusLabel()
  226. def setLabelWidth(self, width):
  227. self.ui.l_name.setFixedWidth(width)
  228. def updateStatusLabel(self):
  229. if self.fMappedCtrl == CONTROL_INDEX_NONE:
  230. text = self.tr("Unmapped")
  231. elif self.fMappedCtrl == CONTROL_INDEX_CV:
  232. text = self.tr("CV export")
  233. elif self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND:
  234. text = self.tr("PBend Ch%i" % (self.fMidiChannel,))
  235. elif self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN:
  236. text = self.tr("MIDI Learn")
  237. else:
  238. text = self.tr("CC%i Ch%i" % (self.fMappedCtrl, self.fMidiChannel))
  239. self.ui.l_status.setText(text)
  240. @pyqtSlot()
  241. def slot_optionsCustomMenu(self):
  242. menu = QMenu(self)
  243. if self.fMappedCtrl == CONTROL_INDEX_NONE:
  244. title = self.tr("Unmapped")
  245. elif self.fMappedCtrl == CONTROL_INDEX_CV:
  246. title = self.tr("Exposed as CV port")
  247. elif self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND:
  248. title = self.tr("Mapped to MIDI Pitchbend, channel %i" % (self.fMidiChannel,))
  249. elif self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN:
  250. title = self.tr("MIDI Learn active")
  251. else:
  252. title = self.tr("Mapped to MIDI control %i, channel %i" % (self.fMappedCtrl, self.fMidiChannel))
  253. if self.fMappedCtrl != CONTROL_INDEX_NONE:
  254. title += " (range: %g-%g)" % (self.fMappedMinimum, self.fMappedMaximum)
  255. actTitle = menu.addAction(title)
  256. actTitle.setEnabled(False)
  257. menu.addSeparator()
  258. actUnmap = menu.addAction(self.tr("Unmap"))
  259. if self.fMappedCtrl == CONTROL_INDEX_NONE:
  260. actUnmap.setCheckable(True)
  261. actUnmap.setChecked(True)
  262. if self.fCanBeInCV:
  263. menu.addSection("CV")
  264. actCV = menu.addAction(self.tr("Expose as CV port"))
  265. if self.fMappedCtrl == CONTROL_INDEX_CV:
  266. actCV.setCheckable(True)
  267. actCV.setChecked(True)
  268. else:
  269. actCV = None
  270. menu.addSection("MIDI")
  271. if not self.ui.widget.isReadOnly():
  272. actLearn = menu.addAction(self.tr("MIDI Learn"))
  273. if self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN:
  274. actLearn.setCheckable(True)
  275. actLearn.setChecked(True)
  276. else:
  277. actLearn = None
  278. menuMIDI = menu.addMenu(self.tr("MIDI Control"))
  279. if self.fMappedCtrl not in (CONTROL_INDEX_NONE,
  280. CONTROL_INDEX_CV,
  281. CONTROL_INDEX_MIDI_PITCHBEND,
  282. CONTROL_INDEX_MIDI_LEARN):
  283. action = menuMIDI.menuAction()
  284. action.setCheckable(True)
  285. action.setChecked(True)
  286. inlist = False
  287. actCCs = []
  288. for cc in MIDI_CC_LIST:
  289. action = menuMIDI.addAction(cc)
  290. actCCs.append(action)
  291. if self.fMappedCtrl >= 0 and self.fMappedCtrl <= MAX_MIDI_CC_LIST_ITEM:
  292. ccx = int(cc.split(" [", 1)[0], 10)
  293. if ccx > self.fMappedCtrl and not inlist:
  294. inlist = True
  295. action = menuMIDI.addAction(self.tr("%02i [0x%02X] (Custom)" % (self.fMappedCtrl,
  296. self.fMappedCtrl)))
  297. action.setCheckable(True)
  298. action.setChecked(True)
  299. actCCs.append(action)
  300. elif ccx == self.fMappedCtrl:
  301. inlist = True
  302. action.setCheckable(True)
  303. action.setChecked(True)
  304. if self.fMappedCtrl > MAX_MIDI_CC_LIST_ITEM and self.fMappedCtrl <= 0x77:
  305. action = menuMIDI.addAction(self.tr("%02i [0x%02X] (Custom)" % (self.fMappedCtrl, self.fMappedCtrl)))
  306. action.setCheckable(True)
  307. action.setChecked(True)
  308. actCCs.append(action)
  309. actCustomCC = menuMIDI.addAction(self.tr("Custom..."))
  310. # TODO
  311. #actPitchbend = menu.addAction(self.tr("MIDI Pitchbend"))
  312. #if self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND:
  313. #actPitchbend.setCheckable(True)
  314. #actPitchbend.setChecked(True)
  315. menuChannel = menu.addMenu(self.tr("MIDI Channel"))
  316. actChannels = []
  317. for i in range(1, 16+1):
  318. action = menuChannel.addAction("%i" % i)
  319. actChannels.append(action)
  320. if self.fMidiChannel == i:
  321. action.setCheckable(True)
  322. action.setChecked(True)
  323. if self.fMappedCtrl != CONTROL_INDEX_NONE:
  324. if self.fMappedCtrl == CONTROL_INDEX_CV:
  325. menu.addSection("Range (Scaled CV input)")
  326. else:
  327. menu.addSection("Range (MIDI bounds)")
  328. actRangeMinimum = menu.addAction(self.tr("Set minimum... (%g)" % self.fMappedMinimum))
  329. actRangeMaximum = menu.addAction(self.tr("Set maximum... (%g)" % self.fMappedMaximum))
  330. else:
  331. actRangeMinimum = actRangeMaximum = None
  332. actSel = menu.exec_(QCursor.pos())
  333. if not actSel:
  334. return
  335. if actSel in actChannels:
  336. channel = int(actSel.text())
  337. self.fMidiChannel = channel
  338. self.updateStatusLabel()
  339. self.midiChannelChanged.emit(self.fParameterId, channel)
  340. return
  341. if actSel == actRangeMinimum:
  342. value, ok = QInputDialog.getDouble(self,
  343. self.tr("Custom Minimum"),
  344. "Custom minimum value to use:",
  345. self.fMappedMinimum,
  346. self.fMinimum if self.fMappedCtrl != CONTROL_INDEX_CV else -9e6,
  347. self.fMaximum if self.fMappedCtrl != CONTROL_INDEX_CV else 9e6,
  348. self.fDecimalPoints)
  349. if not ok:
  350. return
  351. self.fMappedMinimum = value
  352. self.mappedRangeChanged.emit(self.fParameterId, self.fMappedMinimum, self.fMappedMaximum)
  353. return
  354. if actSel == actRangeMaximum:
  355. value, ok = QInputDialog.getDouble(self,
  356. self.tr("Custom Maximum"),
  357. "Custom maximum value to use:",
  358. self.fMappedMaximum,
  359. self.fMinimum if self.fMappedCtrl != CONTROL_INDEX_CV else -9e6,
  360. self.fMaximum if self.fMappedCtrl != CONTROL_INDEX_CV else 9e6,
  361. self.fDecimalPoints)
  362. if not ok:
  363. return
  364. self.fMappedMaximum = value
  365. self.mappedRangeChanged.emit(self.fParameterId, self.fMappedMinimum, self.fMappedMaximum)
  366. return
  367. if actSel == actUnmap:
  368. ctrl = CONTROL_INDEX_NONE
  369. elif actSel == actCV:
  370. ctrl = CONTROL_INDEX_CV
  371. elif actSel == actCustomCC:
  372. value = self.fMappedCtrl if self.fMappedCtrl >= 0x01 and self.fMappedCtrl <= 0x77 else 1
  373. ctrl, ok = QInputDialog.getInt(self,
  374. self.tr("Custom CC"),
  375. "Custom MIDI CC to use:",
  376. value,
  377. 0x01, 0x77, 1)
  378. if not ok:
  379. return
  380. #elif actSel == actPitchbend:
  381. #ctrl = CONTROL_INDEX_MIDI_PITCHBEND
  382. elif actSel == actLearn:
  383. ctrl = CONTROL_INDEX_MIDI_LEARN
  384. elif actSel in actCCs:
  385. ctrl = int(actSel.text().split(" ", 1)[0].replace("&",""), 10)
  386. else:
  387. return
  388. self.fMappedCtrl = ctrl
  389. self.updateStatusLabel()
  390. self.mappedControlChanged.emit(self.fParameterId, ctrl)
  391. @pyqtSlot(bool)
  392. def slot_parameterDragStateChanged(self, touch):
  393. self.host.set_parameter_touch(self.fPluginId, self.fParameterId, touch)
  394. def _textCallBack(self):
  395. return self.host.get_parameter_text(self.fPluginId, self.fParameterId)
  396. def _valueCallBack(self, value):
  397. self.valueChanged.emit(self.fParameterId, value)
  398. # ------------------------------------------------------------------------------------------------------------
  399. # Plugin Editor Parent (Meta class)
  400. class PluginEditParentMeta():
  401. #class PluginEditParentMeta(metaclass=ABCMeta):
  402. @abstractmethod
  403. def editDialogVisibilityChanged(self, pluginId, visible):
  404. raise NotImplementedError
  405. @abstractmethod
  406. def editDialogPluginHintsChanged(self, pluginId, hints):
  407. raise NotImplementedError
  408. @abstractmethod
  409. def editDialogParameterValueChanged(self, pluginId, parameterId, value):
  410. raise NotImplementedError
  411. @abstractmethod
  412. def editDialogProgramChanged(self, pluginId, index):
  413. raise NotImplementedError
  414. @abstractmethod
  415. def editDialogMidiProgramChanged(self, pluginId, index):
  416. raise NotImplementedError
  417. @abstractmethod
  418. def editDialogNotePressed(self, pluginId, note):
  419. raise NotImplementedError
  420. @abstractmethod
  421. def editDialogNoteReleased(self, pluginId, note):
  422. raise NotImplementedError
  423. @abstractmethod
  424. def editDialogMidiActivityChanged(self, pluginId, onOff):
  425. raise NotImplementedError
  426. # ------------------------------------------------------------------------------------------------------------
  427. # Plugin Editor (Built-in)
  428. class PluginEdit(QDialog):
  429. # signals
  430. SIGTERM = pyqtSignal()
  431. SIGUSR1 = pyqtSignal()
  432. def __init__(self, parent, host, pluginId):
  433. QDialog.__init__(self, parent.window() if parent is not None else None)
  434. self.host = host
  435. self.ui = ui_carla_edit.Ui_PluginEdit()
  436. self.ui.setupUi(self)
  437. # -------------------------------------------------------------
  438. # Internal stuff
  439. self.fGeometry = QByteArray()
  440. self.fParent = parent
  441. self.fPluginId = pluginId
  442. self.fPluginInfo = None
  443. self.fCurrentStateFilename = None
  444. self.fControlChannel = round(host.get_internal_parameter_value(pluginId, PARAMETER_CTRL_CHANNEL))
  445. self.fFirstInit = True
  446. self.fParameterList = [] # (type, id, widget)
  447. self.fParametersToUpdate = [] # (id, value)
  448. self.fPlayingNotes = [] # (channel, note)
  449. self.fTabIconOff = QIcon(":/scalable/led_off.svg")
  450. self.fTabIconOn = QIcon(":/scalable/led_yellow.svg")
  451. self.fTabIconTimers = []
  452. # used during testing
  453. self.fIdleTimerId = 0
  454. # -------------------------------------------------------------
  455. # Set-up GUI
  456. labelPluginFont = self.ui.label_plugin.font()
  457. labelPluginFont.setPixelSize(15)
  458. labelPluginFont.setWeight(75)
  459. self.ui.label_plugin.setFont(labelPluginFont)
  460. pluginHints = self.host.get_plugin_info(self.fPluginId)['hints']
  461. self.ui.dial_drywet = ScalableDial(self.ui.dial_drywet, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET)
  462. self.ui.dial_drywet.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_DRYWET))
  463. self.ui.dial_vol = ScalableDial(self.ui.dial_vol, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL)
  464. self.ui.dial_vol.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_VOLUME))
  465. self.ui.dial_b_left = ScalableDial(self.ui.dial_b_left, PARAMETER_BALANCE_LEFT, 100, -1.0, -1.0, 1.0, "L", ScalableDial.CUSTOM_PAINT_MODE_CARLA_L)
  466. self.ui.dial_b_left.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_LEFT))
  467. self.ui.dial_b_right = ScalableDial(self.ui.dial_b_right, PARAMETER_BALANCE_RIGHT, 100, 1.0, -1.0, 1.0, "R", ScalableDial.CUSTOM_PAINT_MODE_CARLA_R)
  468. self.ui.dial_b_right.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_RIGHT))
  469. self.ui.dial_pan = ScalableDial(self.ui.dial_pan, PARAMETER_PANNING, 100, 0, -1.0, 1.0, "Pan", ScalableDial.CUSTOM_PAINT_MODE_CARLA_PAN)
  470. self.ui.dial_pan.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_PANNING))
  471. self.ui.dial_forth = ScalableDial(self.ui.dial_forth, PARAMETER_FORTH, 100, 0, -1.0, 1.0, "Forth", ScalableDial.CUSTOM_PAINT_MODE_CARLA_FORTH)
  472. self.ui.dial_forth.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_FORTH))
  473. self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
  474. self.ui.scrollArea = PixmapKeyboardHArea(self)
  475. self.ui.keyboard = self.ui.scrollArea.keyboard
  476. self.ui.keyboard.setEnabled(self.fControlChannel >= 0)
  477. self.layout().addWidget(self.ui.scrollArea)
  478. self.ui.scrollArea.setEnabled(False)
  479. self.ui.scrollArea.setVisible(False)
  480. # todo
  481. # self.ui.rb_balance.setEnabled(False)
  482. # self.ui.rb_balance.setVisible(False)
  483. # self.ui.rb_pan.setEnabled(False)
  484. # self.ui.rb_pan.setVisible(False)
  485. flags = self.windowFlags()
  486. flags &= ~Qt.WindowContextHelpButtonHint
  487. self.setWindowFlags(flags)
  488. self.reloadAll()
  489. self.fFirstInit = False
  490. # -------------------------------------------------------------
  491. # Set-up connections
  492. self.finished.connect(self.slot_finished)
  493. self.ui.ch_fixed_buffer.clicked.connect(self.slot_optionChanged)
  494. self.ui.ch_force_stereo.clicked.connect(self.slot_optionChanged)
  495. self.ui.ch_map_program_changes.clicked.connect(self.slot_optionChanged)
  496. self.ui.ch_use_chunks.clicked.connect(self.slot_optionChanged)
  497. self.ui.ch_send_notes.clicked.connect(self.slot_optionChanged)
  498. self.ui.ch_send_program_changes.clicked.connect(self.slot_optionChanged)
  499. self.ui.ch_send_control_changes.clicked.connect(self.slot_optionChanged)
  500. self.ui.ch_send_channel_pressure.clicked.connect(self.slot_optionChanged)
  501. self.ui.ch_send_note_aftertouch.clicked.connect(self.slot_optionChanged)
  502. self.ui.ch_send_pitchbend.clicked.connect(self.slot_optionChanged)
  503. self.ui.ch_send_all_sound_off.clicked.connect(self.slot_optionChanged)
  504. self.ui.dial_drywet.realValueChanged.connect(self.slot_dryWetChanged)
  505. self.ui.dial_vol.realValueChanged.connect(self.slot_volumeChanged)
  506. self.ui.dial_b_left.realValueChanged.connect(self.slot_balanceLeftChanged)
  507. self.ui.dial_b_right.realValueChanged.connect(self.slot_balanceRightChanged)
  508. self.ui.dial_pan.realValueChanged.connect(self.slot_panChanged)
  509. self.ui.dial_forth.realValueChanged.connect(self.slot_forthChanged)
  510. self.ui.sb_ctrl_channel.valueChanged.connect(self.slot_ctrlChannelChanged)
  511. self.ui.dial_drywet.customContextMenuRequested.connect(self.slot_knobCustomMenu)
  512. self.ui.dial_vol.customContextMenuRequested.connect(self.slot_knobCustomMenu)
  513. self.ui.dial_b_left.customContextMenuRequested.connect(self.slot_knobCustomMenu)
  514. self.ui.dial_b_right.customContextMenuRequested.connect(self.slot_knobCustomMenu)
  515. self.ui.dial_pan.customContextMenuRequested.connect(self.slot_knobCustomMenu)
  516. self.ui.sb_ctrl_channel.customContextMenuRequested.connect(self.slot_channelCustomMenu)
  517. self.ui.keyboard.noteOn.connect(self.slot_noteOn)
  518. self.ui.keyboard.noteOff.connect(self.slot_noteOff)
  519. self.ui.cb_programs.currentIndexChanged.connect(self.slot_programIndexChanged)
  520. self.ui.cb_midi_programs.currentIndexChanged.connect(self.slot_midiProgramIndexChanged)
  521. self.ui.b_save_state.clicked.connect(self.slot_stateSave)
  522. self.ui.b_load_state.clicked.connect(self.slot_stateLoad)
  523. host.NoteOnCallback.connect(self.slot_handleNoteOnCallback)
  524. host.NoteOffCallback.connect(self.slot_handleNoteOffCallback)
  525. host.UpdateCallback.connect(self.slot_handleUpdateCallback)
  526. host.ReloadInfoCallback.connect(self.slot_handleReloadInfoCallback)
  527. host.ReloadParametersCallback.connect(self.slot_handleReloadParametersCallback)
  528. host.ReloadProgramsCallback.connect(self.slot_handleReloadProgramsCallback)
  529. host.ReloadAllCallback.connect(self.slot_handleReloadAllCallback)
  530. #------------------------------------------------------------------
  531. @pyqtSlot(int, int, int, int)
  532. def slot_handleNoteOnCallback(self, pluginId, channel, note, velocity):
  533. if self.fPluginId != pluginId:
  534. return
  535. if self.fControlChannel == channel:
  536. self.ui.keyboard.sendNoteOn(note, False)
  537. playItem = (channel, note)
  538. if playItem not in self.fPlayingNotes:
  539. self.fPlayingNotes.append(playItem)
  540. if len(self.fPlayingNotes) == 1 and self.fParent is not None:
  541. self.fParent.editDialogMidiActivityChanged(self.fPluginId, True)
  542. @pyqtSlot(int, int, int)
  543. def slot_handleNoteOffCallback(self, pluginId, channel, note):
  544. if self.fPluginId != pluginId:
  545. return
  546. if self.fControlChannel == channel:
  547. self.ui.keyboard.sendNoteOff(note, False)
  548. playItem = (channel, note)
  549. if playItem in self.fPlayingNotes:
  550. self.fPlayingNotes.remove(playItem)
  551. if self.fPlayingNotes and self.fParent is not None:
  552. self.fParent.editDialogMidiActivityChanged(self.fPluginId, False)
  553. @pyqtSlot(int)
  554. def slot_handleUpdateCallback(self, pluginId):
  555. if self.fPluginId == pluginId:
  556. self.updateInfo()
  557. @pyqtSlot(int)
  558. def slot_handleReloadInfoCallback(self, pluginId):
  559. if self.fPluginId == pluginId:
  560. self.reloadInfo()
  561. @pyqtSlot(int)
  562. def slot_handleReloadParametersCallback(self, pluginId):
  563. if self.fPluginId == pluginId:
  564. self.reloadParameters()
  565. @pyqtSlot(int)
  566. def slot_handleReloadProgramsCallback(self, pluginId):
  567. if self.fPluginId == pluginId:
  568. self.reloadPrograms()
  569. @pyqtSlot(int)
  570. def slot_handleReloadAllCallback(self, pluginId):
  571. if self.fPluginId == pluginId:
  572. self.reloadAll()
  573. #------------------------------------------------------------------
  574. def updateInfo(self):
  575. # Update current program text
  576. if self.ui.cb_programs.count() > 0:
  577. pIndex = self.ui.cb_programs.currentIndex()
  578. if pIndex >= 0:
  579. pName = self.host.get_program_name(self.fPluginId, pIndex)
  580. #pName = pName[:40] + (pName[40:] and "...")
  581. self.ui.cb_programs.setItemText(pIndex, pName)
  582. # Update current midi program text
  583. if self.ui.cb_midi_programs.count() > 0:
  584. mpIndex = self.ui.cb_midi_programs.currentIndex()
  585. if mpIndex >= 0:
  586. mpData = self.host.get_midi_program_data(self.fPluginId, mpIndex)
  587. mpBank = mpData['bank']
  588. mpProg = mpData['program']
  589. mpName = mpData['name']
  590. #mpName = mpName[:40] + (mpName[40:] and "...")
  591. self.ui.cb_midi_programs.setItemText(mpIndex, "%03i:%03i - %s" % (mpBank+1, mpProg+1, mpName))
  592. # Update all parameter values
  593. for _, paramId, paramWidget in self.fParameterList:
  594. paramWidget.blockSignals(True)
  595. paramWidget.setValue(self.host.get_current_parameter_value(self.fPluginId, paramId))
  596. paramWidget.blockSignals(False)
  597. # and the internal ones too
  598. self.ui.dial_drywet.blockSignals(True)
  599. self.ui.dial_drywet.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_DRYWET))
  600. self.ui.dial_drywet.blockSignals(False)
  601. self.ui.dial_vol.blockSignals(True)
  602. self.ui.dial_vol.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_VOLUME))
  603. self.ui.dial_vol.blockSignals(False)
  604. self.ui.dial_b_left.blockSignals(True)
  605. self.ui.dial_b_left.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_BALANCE_LEFT))
  606. self.ui.dial_b_left.blockSignals(False)
  607. self.ui.dial_b_right.blockSignals(True)
  608. self.ui.dial_b_right.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_BALANCE_RIGHT))
  609. self.ui.dial_b_right.blockSignals(False)
  610. self.ui.dial_pan.blockSignals(True)
  611. self.ui.dial_pan.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_PANNING))
  612. self.ui.dial_pan.blockSignals(False)
  613. self.ui.dial_forth.blockSignals(True)
  614. self.ui.dial_forth.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_FORTH))
  615. self.ui.dial_forth.blockSignals(False)
  616. self.fControlChannel = round(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_CTRL_CHANNEL))
  617. self.ui.sb_ctrl_channel.blockSignals(True)
  618. self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
  619. self.ui.sb_ctrl_channel.blockSignals(False)
  620. self.ui.keyboard.allNotesOff()
  621. self._updateCtrlPrograms()
  622. self.fParametersToUpdate = []
  623. #------------------------------------------------------------------
  624. def reloadAll(self):
  625. self.fPluginInfo = self.host.get_plugin_info(self.fPluginId)
  626. self.reloadInfo()
  627. self.reloadParameters()
  628. self.reloadPrograms()
  629. if self.fPluginInfo['type'] == PLUGIN_LV2:
  630. self.ui.b_save_state.setEnabled(False)
  631. if not self.ui.scrollArea.isEnabled():
  632. self.resize(self.width(), self.height()-self.ui.scrollArea.height())
  633. #------------------------------------------------------------------
  634. def reloadInfo(self):
  635. realPluginName = self.host.get_real_plugin_name(self.fPluginId)
  636. #audioCountInfo = self.host.get_audio_port_count_info(self.fPluginId)
  637. midiCountInfo = self.host.get_midi_port_count_info(self.fPluginId)
  638. #paramCountInfo = self.host.get_parameter_count_info(self.fPluginId)
  639. pluginHints = self.fPluginInfo['hints']
  640. self.ui.le_type.setText(getPluginTypeAsString(self.fPluginInfo['type']))
  641. self.ui.label_name.setEnabled(bool(realPluginName))
  642. self.ui.le_name.setEnabled(bool(realPluginName))
  643. self.ui.le_name.setText(realPluginName)
  644. self.ui.le_name.setToolTip(realPluginName)
  645. self.ui.label_label.setEnabled(bool(self.fPluginInfo['label']))
  646. self.ui.le_label.setEnabled(bool(self.fPluginInfo['label']))
  647. self.ui.le_label.setText(self.fPluginInfo['label'])
  648. self.ui.le_label.setToolTip(self.fPluginInfo['label'])
  649. self.ui.label_maker.setEnabled(bool(self.fPluginInfo['maker']))
  650. self.ui.le_maker.setEnabled(bool(self.fPluginInfo['maker']))
  651. self.ui.le_maker.setText(self.fPluginInfo['maker'])
  652. self.ui.le_maker.setToolTip(self.fPluginInfo['maker'])
  653. self.ui.label_copyright.setEnabled(bool(self.fPluginInfo['copyright']))
  654. self.ui.le_copyright.setEnabled(bool(self.fPluginInfo['copyright']))
  655. self.ui.le_copyright.setText(self.fPluginInfo['copyright'])
  656. self.ui.le_copyright.setToolTip(self.fPluginInfo['copyright'])
  657. self.ui.label_unique_id.setEnabled(bool(self.fPluginInfo['uniqueId']))
  658. self.ui.le_unique_id.setEnabled(bool(self.fPluginInfo['uniqueId']))
  659. self.ui.le_unique_id.setText(str(self.fPluginInfo['uniqueId']))
  660. self.ui.le_unique_id.setToolTip(str(self.fPluginInfo['uniqueId']))
  661. self.ui.label_plugin.setText("\n%s\n" % (self.fPluginInfo['name'] or "(none)"))
  662. self.setWindowTitle(self.fPluginInfo['name'] or "(none)")
  663. self.ui.dial_drywet.setEnabled(pluginHints & PLUGIN_CAN_DRYWET)
  664. self.ui.dial_vol.setEnabled(pluginHints & PLUGIN_CAN_VOLUME)
  665. self.ui.dial_b_left.setEnabled(pluginHints & PLUGIN_CAN_BALANCE)
  666. self.ui.dial_b_right.setEnabled(pluginHints & PLUGIN_CAN_BALANCE)
  667. self.ui.dial_pan.setEnabled(pluginHints & PLUGIN_CAN_PANNING)
  668. self.ui.dial_forth.setEnabled(pluginHints & PLUGIN_CAN_FORTH)
  669. optsAvailable = self.fPluginInfo['optionsAvailable']
  670. optsEnabled = self.fPluginInfo['optionsEnabled']
  671. self.ui.ch_use_chunks.setEnabled(optsAvailable & PLUGIN_OPTION_USE_CHUNKS)
  672. self.ui.ch_use_chunks.setChecked(optsEnabled & PLUGIN_OPTION_USE_CHUNKS)
  673. self.ui.ch_fixed_buffer.setEnabled(optsAvailable & PLUGIN_OPTION_FIXED_BUFFERS)
  674. self.ui.ch_fixed_buffer.setChecked(optsEnabled & PLUGIN_OPTION_FIXED_BUFFERS)
  675. self.ui.ch_force_stereo.setEnabled(optsAvailable & PLUGIN_OPTION_FORCE_STEREO)
  676. self.ui.ch_force_stereo.setChecked(optsEnabled & PLUGIN_OPTION_FORCE_STEREO)
  677. self.ui.ch_map_program_changes.setEnabled(optsAvailable & PLUGIN_OPTION_MAP_PROGRAM_CHANGES)
  678. self.ui.ch_map_program_changes.setChecked(optsEnabled & PLUGIN_OPTION_MAP_PROGRAM_CHANGES)
  679. self.ui.ch_send_notes.setEnabled(optsAvailable & PLUGIN_OPTION_SKIP_SENDING_NOTES)
  680. self.ui.ch_send_notes.setChecked((self.ui.ch_send_notes.isEnabled() and
  681. (optsEnabled & PLUGIN_OPTION_SKIP_SENDING_NOTES) == 0x0))
  682. self.ui.ch_send_control_changes.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_CONTROL_CHANGES)
  683. self.ui.ch_send_control_changes.setChecked(optsEnabled & PLUGIN_OPTION_SEND_CONTROL_CHANGES)
  684. self.ui.ch_send_channel_pressure.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_CHANNEL_PRESSURE)
  685. self.ui.ch_send_channel_pressure.setChecked(optsEnabled & PLUGIN_OPTION_SEND_CHANNEL_PRESSURE)
  686. self.ui.ch_send_note_aftertouch.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH)
  687. self.ui.ch_send_note_aftertouch.setChecked(optsEnabled & PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH)
  688. self.ui.ch_send_pitchbend.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_PITCHBEND)
  689. self.ui.ch_send_pitchbend.setChecked(optsEnabled & PLUGIN_OPTION_SEND_PITCHBEND)
  690. self.ui.ch_send_all_sound_off.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
  691. self.ui.ch_send_all_sound_off.setChecked(optsEnabled & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
  692. canSendPrograms = bool((optsAvailable & PLUGIN_OPTION_SEND_PROGRAM_CHANGES) != 0 and
  693. (optsEnabled & PLUGIN_OPTION_MAP_PROGRAM_CHANGES) == 0)
  694. self.ui.ch_send_program_changes.setEnabled(canSendPrograms)
  695. self.ui.ch_send_program_changes.setChecked(optsEnabled & PLUGIN_OPTION_SEND_PROGRAM_CHANGES)
  696. self.ui.sw_programs.setCurrentIndex(0 if self.fPluginInfo['type'] in (PLUGIN_VST2, PLUGIN_SFZ) else 1)
  697. # Show/hide keyboard
  698. showKeyboard = (self.fPluginInfo['category'] == PLUGIN_CATEGORY_SYNTH or midiCountInfo['ins'] > 0)
  699. self.ui.scrollArea.setEnabled(showKeyboard)
  700. self.ui.scrollArea.setVisible(showKeyboard)
  701. # Force-update parent for new hints
  702. if self.fParent is not None and not self.fFirstInit:
  703. self.fParent.editDialogPluginHintsChanged(self.fPluginId, pluginHints)
  704. def reloadParameters(self):
  705. # Reset
  706. self.fParameterList = []
  707. self.fParametersToUpdate = []
  708. self.fTabIconTimers = []
  709. # Save current tab state
  710. tabIndex = self.ui.tabWidget.currentIndex()
  711. tabWidget = self.ui.tabWidget.currentWidget()
  712. scrollVal = tabWidget.verticalScrollBar().value() if isinstance(tabWidget, QScrollArea) else None
  713. del tabWidget
  714. # Remove all previous parameters
  715. for _ in range(self.ui.tabWidget.count()-1):
  716. self.ui.tabWidget.widget(1).deleteLater()
  717. self.ui.tabWidget.removeTab(1)
  718. parameterCount = self.host.get_parameter_count(self.fPluginId)
  719. # -----------------------------------------------------------------
  720. if parameterCount <= 0:
  721. return
  722. # -----------------------------------------------------------------
  723. paramInputList = []
  724. paramOutputList = []
  725. paramInputWidth = 0
  726. paramOutputWidth = 0
  727. unusedParameters = 0
  728. paramInputListFull = [] # ([params], width)
  729. paramOutputListFull = [] # ([params], width)
  730. for i in range(parameterCount):
  731. if i - unusedParameters == self.host.maxParameters:
  732. break
  733. paramData = self.host.get_parameter_data(self.fPluginId, i)
  734. if paramData['type'] not in (PARAMETER_INPUT, PARAMETER_OUTPUT):
  735. unusedParameters += 1
  736. continue
  737. if (paramData['hints'] & PARAMETER_IS_ENABLED) == 0:
  738. unusedParameters += 1
  739. continue
  740. paramInfo = self.host.get_parameter_info(self.fPluginId, i)
  741. paramRanges = self.host.get_parameter_ranges(self.fPluginId, i)
  742. paramValue = self.host.get_current_parameter_value(self.fPluginId, i)
  743. parameter = {
  744. 'type': paramData['type'],
  745. 'hints': paramData['hints'],
  746. 'name': paramInfo['name'],
  747. 'unit': paramInfo['unit'],
  748. 'scalePoints': [],
  749. 'index': paramData['index'],
  750. 'default': paramRanges['def'],
  751. 'minimum': paramRanges['min'],
  752. 'maximum': paramRanges['max'],
  753. 'step': paramRanges['step'],
  754. 'stepSmall': paramRanges['stepSmall'],
  755. 'stepLarge': paramRanges['stepLarge'],
  756. 'mappedControlIndex': paramData['mappedControlIndex'],
  757. 'mappedMinimum': paramData['mappedMinimum'],
  758. 'mappedMaximum': paramData['mappedMaximum'],
  759. 'midiChannel': paramData['midiChannel']+1,
  760. 'comment': paramInfo['comment'],
  761. 'groupName': paramInfo['groupName'],
  762. 'current': paramValue
  763. }
  764. for j in range(paramInfo['scalePointCount']):
  765. scalePointInfo = self.host.get_parameter_scalepoint_info(self.fPluginId, i, j)
  766. parameter['scalePoints'].append({
  767. 'value': scalePointInfo['value'],
  768. 'label': scalePointInfo['label']
  769. })
  770. #parameter['name'] = parameter['name'][:30] + (parameter['name'][30:] and "...")
  771. # -----------------------------------------------------------------
  772. # Get width values, in packs of 20
  773. if parameter['type'] == PARAMETER_INPUT:
  774. paramInputWidthTMP = fontMetricsHorizontalAdvance(self.fontMetrics(), parameter['name'])
  775. if paramInputWidthTMP > paramInputWidth:
  776. paramInputWidth = paramInputWidthTMP
  777. paramInputList.append(parameter)
  778. else:
  779. paramOutputWidthTMP = fontMetricsHorizontalAdvance(self.fontMetrics(), parameter['name'])
  780. if paramOutputWidthTMP > paramOutputWidth:
  781. paramOutputWidth = paramOutputWidthTMP
  782. paramOutputList.append(parameter)
  783. paramInputListFull.append((paramInputList, paramInputWidth))
  784. paramOutputListFull.append((paramOutputList, paramOutputWidth))
  785. # Create parameter tabs + widgets
  786. self._createParameterWidgets(PARAMETER_INPUT, paramInputListFull, self.tr("Parameters"))
  787. self._createParameterWidgets(PARAMETER_OUTPUT, paramOutputListFull, self.tr("Outputs"))
  788. # Create full parameter list table tab
  789. self._createParameterXrayTab(self.tr("XRay"))
  790. # Create experimental description tab WORKINPROGRESS NOTE discussions/1967, pull/1961
  791. self._createDescriptionTab(self.tr("Description"))
  792. # Restore tab state
  793. if tabIndex < self.ui.tabWidget.count():
  794. self.ui.tabWidget.setCurrentIndex(tabIndex)
  795. if scrollVal is not None:
  796. self.ui.tabWidget.currentWidget().verticalScrollBar().setValue(scrollVal)
  797. def reloadPrograms(self):
  798. # Programs
  799. self.ui.cb_programs.blockSignals(True)
  800. self.ui.cb_programs.clear()
  801. programCount = self.host.get_program_count(self.fPluginId)
  802. if programCount > 0:
  803. self.ui.cb_programs.setEnabled(True)
  804. self.ui.label_programs.setEnabled(True)
  805. for i in range(programCount):
  806. pName = self.host.get_program_name(self.fPluginId, i)
  807. #pName = pName[:40] + (pName[40:] and "...")
  808. self.ui.cb_programs.addItem(pName)
  809. self.ui.cb_programs.setCurrentIndex(self.host.get_current_program_index(self.fPluginId))
  810. else:
  811. self.ui.cb_programs.setEnabled(False)
  812. self.ui.label_programs.setEnabled(False)
  813. self.ui.cb_programs.blockSignals(False)
  814. # MIDI Programs
  815. self.ui.cb_midi_programs.blockSignals(True)
  816. self.ui.cb_midi_programs.clear()
  817. midiProgramCount = self.host.get_midi_program_count(self.fPluginId)
  818. if midiProgramCount > 0:
  819. self.ui.cb_midi_programs.setEnabled(True)
  820. self.ui.label_midi_programs.setEnabled(True)
  821. for i in range(midiProgramCount):
  822. mpData = self.host.get_midi_program_data(self.fPluginId, i)
  823. mpBank = mpData['bank']
  824. mpProg = mpData['program']
  825. mpName = mpData['name']
  826. #mpName = mpName[:40] + (mpName[40:] and "...")
  827. self.ui.cb_midi_programs.addItem("%03i:%03i - %s" % (mpBank+1, mpProg+1, mpName))
  828. self.ui.cb_midi_programs.setCurrentIndex(self.host.get_current_midi_program_index(self.fPluginId))
  829. else:
  830. self.ui.cb_midi_programs.setEnabled(False)
  831. self.ui.label_midi_programs.setEnabled(False)
  832. self.ui.cb_midi_programs.blockSignals(False)
  833. self.ui.sw_programs.setEnabled(programCount > 0 or midiProgramCount > 0)
  834. if self.fPluginInfo['type'] == PLUGIN_LV2:
  835. self.ui.b_load_state.setEnabled(programCount > 0)
  836. #------------------------------------------------------------------
  837. def clearNotes(self):
  838. self.fPlayingNotes = []
  839. self.ui.keyboard.allNotesOff()
  840. def noteOn(self, channel, note, velocity):
  841. if self.fControlChannel == channel:
  842. self.ui.keyboard.sendNoteOn(note, False)
  843. def noteOff(self, channel, note):
  844. if self.fControlChannel == channel:
  845. self.ui.keyboard.sendNoteOff(note, False)
  846. #------------------------------------------------------------------
  847. def getHints(self):
  848. return self.fPluginInfo['hints']
  849. def setPluginId(self, idx):
  850. self.fPluginId = idx
  851. def setName(self, name):
  852. self.fPluginInfo['name'] = name
  853. self.ui.label_plugin.setText("\n%s\n" % name)
  854. self.setWindowTitle(name)
  855. #------------------------------------------------------------------
  856. def setParameterValue(self, parameterId, value):
  857. for paramItem in self.fParametersToUpdate:
  858. if paramItem[0] == parameterId:
  859. paramItem[1] = value
  860. break
  861. else:
  862. self.fParametersToUpdate.append([parameterId, value])
  863. def setParameterDefault(self, parameterId, value):
  864. for _, paramId, paramWidget in self.fParameterList:
  865. if paramId == parameterId:
  866. paramWidget.setDefault(value)
  867. break
  868. def setParameterMappedControlIndex(self, parameterId, control):
  869. for _, paramId, paramWidget in self.fParameterList:
  870. if paramId == parameterId:
  871. paramWidget.setMappedControlIndex(control)
  872. break
  873. def setParameterMappedRange(self, parameterId, minimum, maximum):
  874. for _, paramId, paramWidget in self.fParameterList:
  875. if paramId == parameterId:
  876. paramWidget.setMappedRange(minimum, maximum)
  877. break
  878. def setParameterMidiChannel(self, parameterId, channel):
  879. for _, paramId, paramWidget in self.fParameterList:
  880. if paramId == parameterId:
  881. paramWidget.setMidiChannel(channel+1)
  882. break
  883. def setProgram(self, index):
  884. self.ui.cb_programs.blockSignals(True)
  885. self.ui.cb_programs.setCurrentIndex(index)
  886. self.ui.cb_programs.blockSignals(False)
  887. self._updateParameterValues()
  888. def setMidiProgram(self, index):
  889. self.ui.cb_midi_programs.blockSignals(True)
  890. self.ui.cb_midi_programs.setCurrentIndex(index)
  891. self.ui.cb_midi_programs.blockSignals(False)
  892. self._updateParameterValues()
  893. def setOption(self, option, yesNo):
  894. if option == PLUGIN_OPTION_USE_CHUNKS:
  895. widget = self.ui.ch_use_chunks
  896. elif option == PLUGIN_OPTION_FIXED_BUFFERS:
  897. widget = self.ui.ch_fixed_buffer
  898. elif option == PLUGIN_OPTION_FORCE_STEREO:
  899. widget = self.ui.ch_force_stereo
  900. elif option == PLUGIN_OPTION_MAP_PROGRAM_CHANGES:
  901. widget = self.ui.ch_map_program_changes
  902. elif option == PLUGIN_OPTION_SKIP_SENDING_NOTES:
  903. widget = self.ui.ch_send_notes
  904. yesNo = not yesNo
  905. elif option == PLUGIN_OPTION_SEND_PROGRAM_CHANGES:
  906. widget = self.ui.ch_send_program_changes
  907. elif option == PLUGIN_OPTION_SEND_CONTROL_CHANGES:
  908. widget = self.ui.ch_send_control_changes
  909. elif option == PLUGIN_OPTION_SEND_CHANNEL_PRESSURE:
  910. widget = self.ui.ch_send_channel_pressure
  911. elif option == PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH:
  912. widget = self.ui.ch_send_note_aftertouch
  913. elif option == PLUGIN_OPTION_SEND_PITCHBEND:
  914. widget = self.ui.ch_send_pitchbend
  915. elif option == PLUGIN_OPTION_SEND_ALL_SOUND_OFF:
  916. widget = self.ui.ch_send_all_sound_off
  917. else:
  918. return
  919. widget.blockSignals(True)
  920. widget.setChecked(yesNo)
  921. widget.blockSignals(False)
  922. #------------------------------------------------------------------
  923. def setVisible(self, yesNo):
  924. if yesNo:
  925. if not self.fGeometry.isNull():
  926. self.restoreGeometry(self.fGeometry)
  927. else:
  928. self.fGeometry = self.saveGeometry()
  929. QDialog.setVisible(self, yesNo)
  930. if CARLA_OS_MAC and yesNo:
  931. parent = self.parent()
  932. if parent is None:
  933. return
  934. gCarla.utils.cocoa_set_transient_window_for(self.windowHandle().winId(), parent.windowHandle().winId())
  935. #------------------------------------------------------------------
  936. def idleSlow(self):
  937. # Check Tab icons
  938. for i in range(len(self.fTabIconTimers)):
  939. if self.fTabIconTimers[i] == ICON_STATE_ON:
  940. self.fTabIconTimers[i] = ICON_STATE_WAIT
  941. elif self.fTabIconTimers[i] == ICON_STATE_WAIT:
  942. self.fTabIconTimers[i] = ICON_STATE_OFF
  943. elif self.fTabIconTimers[i] == ICON_STATE_OFF:
  944. self.fTabIconTimers[i] = ICON_STATE_NULL
  945. self.ui.tabWidget.setTabIcon(i+1, self.fTabIconOff)
  946. # Check parameters needing update
  947. for index, value in self.fParametersToUpdate:
  948. if index == PARAMETER_DRYWET:
  949. self.ui.dial_drywet.blockSignals(True)
  950. self.ui.dial_drywet.setValue(value)
  951. self.ui.dial_drywet.blockSignals(False)
  952. elif index == PARAMETER_VOLUME:
  953. self.ui.dial_vol.blockSignals(True)
  954. self.ui.dial_vol.setValue(value)
  955. self.ui.dial_vol.blockSignals(False)
  956. elif index == PARAMETER_BALANCE_LEFT:
  957. self.ui.dial_b_left.blockSignals(True)
  958. self.ui.dial_b_left.setValue(value)
  959. self.ui.dial_b_left.blockSignals(False)
  960. elif index == PARAMETER_BALANCE_RIGHT:
  961. self.ui.dial_b_right.blockSignals(True)
  962. self.ui.dial_b_right.setValue(value)
  963. self.ui.dial_b_right.blockSignals(False)
  964. elif index == PARAMETER_PANNING:
  965. self.ui.dial_pan.blockSignals(True)
  966. self.ui.dial_pan.setValue(value)
  967. self.ui.dial_pan.blockSignals(False)
  968. elif index == PARAMETER_FORTH:
  969. self.ui.dial_forth.blockSignals(True)
  970. self.ui.dial_forth.setValue(value)
  971. self.ui.dial_forth.blockSignals(False)
  972. elif index == PARAMETER_CTRL_CHANNEL:
  973. self.fControlChannel = round(value)
  974. self.ui.sb_ctrl_channel.blockSignals(True)
  975. self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
  976. self.ui.sb_ctrl_channel.blockSignals(False)
  977. self.ui.keyboard.allNotesOff()
  978. self._updateCtrlPrograms()
  979. elif index >= 0:
  980. for paramType, paramId, paramWidget in self.fParameterList:
  981. if paramId != index:
  982. continue
  983. # FIXME see below
  984. if paramType != PARAMETER_INPUT:
  985. continue
  986. paramWidget.blockSignals(True)
  987. paramWidget.setValue(value)
  988. paramWidget.blockSignals(False)
  989. #if paramType == PARAMETER_INPUT:
  990. tabIndex = paramWidget.getTabIndex()
  991. if self.fTabIconTimers[tabIndex-1] == ICON_STATE_NULL:
  992. self.ui.tabWidget.setTabIcon(tabIndex, self.fTabIconOn)
  993. self.fTabIconTimers[tabIndex-1] = ICON_STATE_ON
  994. break
  995. # Clear all parameters
  996. self.fParametersToUpdate = []
  997. # Update parameter outputs | FIXME needed?
  998. for paramType, paramId, paramWidget in self.fParameterList:
  999. if paramType != PARAMETER_OUTPUT:
  1000. continue
  1001. paramWidget.blockSignals(True)
  1002. paramWidget.setValue(self.host.get_current_parameter_value(self.fPluginId, paramId))
  1003. paramWidget.blockSignals(False)
  1004. #------------------------------------------------------------------
  1005. @pyqtSlot()
  1006. def slot_stateSave(self):
  1007. if self.fPluginInfo['type'] == PLUGIN_LV2:
  1008. # TODO
  1009. return
  1010. if self.fCurrentStateFilename:
  1011. askTry = QMessageBox.question(self,
  1012. self.tr("Overwrite?"),
  1013. self.tr("Overwrite previously created file?"),
  1014. QMessageBox.Ok|QMessageBox.Cancel)
  1015. if askTry == QMessageBox.Ok:
  1016. self.host.save_plugin_state(self.fPluginId, self.fCurrentStateFilename)
  1017. return
  1018. self.fCurrentStateFilename = None
  1019. fileFilter = self.tr("Carla State File (*.carxs)")
  1020. filename, _ = QFileDialog.getSaveFileName(self, self.tr("Save Plugin State File"), filter=fileFilter)
  1021. # FIXME use ok value, test if it works as expected
  1022. if not filename:
  1023. return
  1024. if not filename.lower().endswith(".carxs"):
  1025. filename += ".carxs"
  1026. self.fCurrentStateFilename = filename
  1027. self.host.save_plugin_state(self.fPluginId, self.fCurrentStateFilename)
  1028. @pyqtSlot()
  1029. def slot_stateLoad(self):
  1030. if self.fPluginInfo['type'] == PLUGIN_LV2:
  1031. presetList = []
  1032. for i in range(self.host.get_program_count(self.fPluginId)):
  1033. presetList.append("%03i - %s" % (i+1, self.host.get_program_name(self.fPluginId, i)))
  1034. ret = QInputDialog.getItem(self,
  1035. self.tr("Open LV2 Preset"),
  1036. self.tr("Select an LV2 Preset:"),
  1037. presetList, 0, False)
  1038. if ret[1]:
  1039. index = int(ret[0].split(" - ", 1)[0])-1
  1040. self.host.set_program(self.fPluginId, index)
  1041. self.setMidiProgram(-1)
  1042. return
  1043. fileFilter = self.tr("Carla State File (*.carxs)")
  1044. filename, _ = QFileDialog.getOpenFileName(self, self.tr("Open Plugin State File"), filter=fileFilter)
  1045. # FIXME use ok value, test if it works as expected
  1046. if not filename:
  1047. return
  1048. self.fCurrentStateFilename = filename
  1049. self.host.load_plugin_state(self.fPluginId, self.fCurrentStateFilename)
  1050. #------------------------------------------------------------------
  1051. @pyqtSlot(bool)
  1052. def slot_optionChanged(self, clicked):
  1053. sender = self.sender()
  1054. if sender == self.ui.ch_use_chunks:
  1055. option = PLUGIN_OPTION_USE_CHUNKS
  1056. elif sender == self.ui.ch_fixed_buffer:
  1057. option = PLUGIN_OPTION_FIXED_BUFFERS
  1058. elif sender == self.ui.ch_force_stereo:
  1059. option = PLUGIN_OPTION_FORCE_STEREO
  1060. elif sender == self.ui.ch_map_program_changes:
  1061. option = PLUGIN_OPTION_MAP_PROGRAM_CHANGES
  1062. elif sender == self.ui.ch_send_notes:
  1063. option = PLUGIN_OPTION_SKIP_SENDING_NOTES
  1064. clicked = not clicked
  1065. elif sender == self.ui.ch_send_program_changes:
  1066. option = PLUGIN_OPTION_SEND_PROGRAM_CHANGES
  1067. elif sender == self.ui.ch_send_control_changes:
  1068. option = PLUGIN_OPTION_SEND_CONTROL_CHANGES
  1069. elif sender == self.ui.ch_send_channel_pressure:
  1070. option = PLUGIN_OPTION_SEND_CHANNEL_PRESSURE
  1071. elif sender == self.ui.ch_send_note_aftertouch:
  1072. option = PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH
  1073. elif sender == self.ui.ch_send_pitchbend:
  1074. option = PLUGIN_OPTION_SEND_PITCHBEND
  1075. elif sender == self.ui.ch_send_all_sound_off:
  1076. option = PLUGIN_OPTION_SEND_ALL_SOUND_OFF
  1077. else:
  1078. return
  1079. #--------------------------------------------------------------
  1080. # handle map-program-changes and send-program-changes conflict
  1081. if option == PLUGIN_OPTION_MAP_PROGRAM_CHANGES and clicked:
  1082. self.ui.ch_send_program_changes.setEnabled(False)
  1083. # disable send-program-changes if needed
  1084. if self.ui.ch_send_program_changes.isChecked():
  1085. self.host.set_option(self.fPluginId, PLUGIN_OPTION_SEND_PROGRAM_CHANGES, False)
  1086. #--------------------------------------------------------------
  1087. # set option
  1088. self.host.set_option(self.fPluginId, option, clicked)
  1089. #--------------------------------------------------------------
  1090. # handle map-program-changes and send-program-changes conflict
  1091. if option == PLUGIN_OPTION_MAP_PROGRAM_CHANGES and not clicked:
  1092. self.ui.ch_send_program_changes.setEnabled(
  1093. self.fPluginInfo['optionsAvailable'] & PLUGIN_OPTION_SEND_PROGRAM_CHANGES)
  1094. # restore send-program-changes if needed
  1095. if self.ui.ch_send_program_changes.isChecked():
  1096. self.host.set_option(self.fPluginId, PLUGIN_OPTION_SEND_PROGRAM_CHANGES, True)
  1097. #------------------------------------------------------------------
  1098. @pyqtSlot(float)
  1099. def slot_dryWetChanged(self, value):
  1100. self.host.set_drywet(self.fPluginId, value)
  1101. if self.fParent is not None:
  1102. self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_DRYWET, value)
  1103. @pyqtSlot(float)
  1104. def slot_volumeChanged(self, value):
  1105. self.host.set_volume(self.fPluginId, value)
  1106. if self.fParent is not None:
  1107. self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_VOLUME, value)
  1108. @pyqtSlot(float)
  1109. def slot_balanceLeftChanged(self, value):
  1110. self.host.set_balance_left(self.fPluginId, value)
  1111. if self.fParent is not None:
  1112. self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_BALANCE_LEFT, value)
  1113. @pyqtSlot(float)
  1114. def slot_balanceRightChanged(self, value):
  1115. self.host.set_balance_right(self.fPluginId, value)
  1116. if self.fParent is not None:
  1117. self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_BALANCE_RIGHT, value)
  1118. @pyqtSlot(float)
  1119. def slot_panChanged(self, value):
  1120. self.host.set_panning(self.fPluginId, value)
  1121. if self.fParent is not None:
  1122. self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_PANNING, value)
  1123. @pyqtSlot(float)
  1124. def slot_forthChanged(self, value):
  1125. self.host.set_forth(self.fPluginId, value)
  1126. if self.fParent is not None:
  1127. self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_FORTH, value)
  1128. @pyqtSlot(int)
  1129. def slot_ctrlChannelChanged(self, value):
  1130. self.fControlChannel = value-1
  1131. self.host.set_ctrl_channel(self.fPluginId, self.fControlChannel)
  1132. self.ui.keyboard.allNotesOff()
  1133. self._updateCtrlPrograms()
  1134. #------------------------------------------------------------------
  1135. @pyqtSlot(int, float)
  1136. def slot_parameterValueChanged(self, parameterId, value):
  1137. self.host.set_parameter_value(self.fPluginId, parameterId, value)
  1138. if self.fParent is not None:
  1139. self.fParent.editDialogParameterValueChanged(self.fPluginId, parameterId, value)
  1140. @pyqtSlot(int, int)
  1141. def slot_parameterMappedControlChanged(self, parameterId, control):
  1142. self.host.set_parameter_mapped_control_index(self.fPluginId, parameterId, control)
  1143. @pyqtSlot(int, float, float)
  1144. def slot_parameterMappedRangeChanged(self, parameterId, minimum, maximum):
  1145. self.host.set_parameter_mapped_range(self.fPluginId, parameterId, minimum, maximum)
  1146. @pyqtSlot(int, int)
  1147. def slot_parameterMidiChannelChanged(self, parameterId, channel):
  1148. self.host.set_parameter_midi_channel(self.fPluginId, parameterId, channel-1)
  1149. #------------------------------------------------------------------
  1150. @pyqtSlot(int)
  1151. def slot_programIndexChanged(self, index):
  1152. self.host.set_program(self.fPluginId, index)
  1153. if self.fParent is not None:
  1154. self.fParent.editDialogProgramChanged(self.fPluginId, index)
  1155. self._updateParameterValues()
  1156. @pyqtSlot(int)
  1157. def slot_midiProgramIndexChanged(self, index):
  1158. self.host.set_midi_program(self.fPluginId, index)
  1159. if self.fParent is not None:
  1160. self.fParent.editDialogMidiProgramChanged(self.fPluginId, index)
  1161. self._updateParameterValues()
  1162. #------------------------------------------------------------------
  1163. @pyqtSlot(int)
  1164. def slot_noteOn(self, note):
  1165. if self.fControlChannel >= 0:
  1166. self.host.send_midi_note(self.fPluginId, self.fControlChannel, note, 100)
  1167. if self.fParent is not None:
  1168. self.fParent.editDialogNotePressed(self.fPluginId, note)
  1169. @pyqtSlot(int)
  1170. def slot_noteOff(self, note):
  1171. if self.fControlChannel >= 0:
  1172. self.host.send_midi_note(self.fPluginId, self.fControlChannel, note, 0)
  1173. if self.fParent is not None:
  1174. self.fParent.editDialogNoteReleased(self.fPluginId, note)
  1175. #------------------------------------------------------------------
  1176. @pyqtSlot()
  1177. def slot_finished(self):
  1178. if self.fParent is not None:
  1179. self.fParent.editDialogVisibilityChanged(self.fPluginId, False)
  1180. #------------------------------------------------------------------
  1181. @pyqtSlot()
  1182. def slot_knobCustomMenu(self):
  1183. # jpka: NOTE now Edit knobs are also know their constraints, so it's worth to set values as normal ones.
  1184. sender = self.sender()
  1185. index = sender.fIndex
  1186. minimum = sender.fMinimum
  1187. maximum = sender.fMaximum
  1188. current = sender.fRealValue
  1189. label = sender.fLabel
  1190. default = sender.fDefault
  1191. unit = sender.fUnit
  1192. step = stepSmall = 1.0
  1193. if index < PARAMETER_NULL:
  1194. percent = 100.0
  1195. else:
  1196. percent = 1
  1197. textReset = self.tr("Reset (" + strLim(default * percent) + unit + ")\tR, Middle click")
  1198. textMinim = self.tr("Set to Minimum (" + strLim(minimum * percent) + unit + ")\t0")
  1199. textMaxim = self.tr("Set to Maximum (" + strLim(maximum * percent) + unit + ")\tEnd")
  1200. if sender.fIsButton:
  1201. editHotKey = "E"
  1202. else:
  1203. editHotKey = "Enter, Double click"
  1204. menu = QMenu(self)
  1205. actReset = menu.addAction(textReset)
  1206. menu.addSeparator()
  1207. actMinimum = menu.addAction(textMinim)
  1208. actCenter = menu.addAction(self.tr("Set to Center\t5"))
  1209. actMaximum = menu.addAction(textMaxim)
  1210. menu.addSeparator()
  1211. actSet = menu.addAction(self.tr("Set value...\t" + editHotKey))
  1212. if index > PARAMETER_NULL or index not in (PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING, PARAMETER_FORTH):
  1213. menu.removeAction(actCenter)
  1214. actSelected = menu.exec_(QCursor.pos())
  1215. if actSelected == actSet:
  1216. paramInfo = self.host.get_parameter_info(self.fPluginId, index)
  1217. paramRanges = self.host.get_parameter_ranges(self.fPluginId, index)
  1218. scalePoints = []
  1219. for i in range(paramInfo['scalePointCount']):
  1220. scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, index, i))
  1221. if sender.fIsInteger:
  1222. step = max(1, int((maximum - minimum)/100))
  1223. stepSmall = max(1, int(step/10))
  1224. else:
  1225. step = paramRanges['step'] * percent
  1226. stepSmall = paramRanges['stepSmall'] * percent
  1227. dialog = CustomInputDialog(self, label, current * percent, minimum * percent, maximum * percent, step, stepSmall, scalePoints, "", "", unit)
  1228. if not dialog.exec_():
  1229. return
  1230. value = dialog.returnValue() / percent
  1231. elif actSelected == actMinimum:
  1232. value = minimum
  1233. elif actSelected == actMaximum:
  1234. value = maximum
  1235. elif actSelected == actReset:
  1236. value = default
  1237. elif actSelected == actCenter:
  1238. value = 0.0
  1239. else:
  1240. return
  1241. sender.setValue(value, True)
  1242. #------------------------------------------------------------------
  1243. @pyqtSlot()
  1244. def slot_channelCustomMenu(self):
  1245. menu = QMenu(self)
  1246. actNone = menu.addAction(self.tr("None"))
  1247. if self.fControlChannel+1 == 0:
  1248. actNone.setCheckable(True)
  1249. actNone.setChecked(True)
  1250. for i in range(1, 16+1):
  1251. action = menu.addAction("%i" % i)
  1252. if self.fControlChannel+1 == i:
  1253. action.setCheckable(True)
  1254. action.setChecked(True)
  1255. actSel = menu.exec_(QCursor.pos())
  1256. if not actSel:
  1257. pass
  1258. elif actSel == actNone:
  1259. self.ui.sb_ctrl_channel.setValue(0)
  1260. elif actSel:
  1261. selChannel = int(actSel.text())
  1262. self.ui.sb_ctrl_channel.setValue(selChannel)
  1263. #------------------------------------------------------------------
  1264. def _createParameterWidgets(self, paramType, paramListFull, tabPageName):
  1265. groupWidgets = {}
  1266. for paramList, width in paramListFull:
  1267. if not paramList:
  1268. break
  1269. tabIndex = self.ui.tabWidget.count()
  1270. scrollArea = QScrollArea(self.ui.tabWidget)
  1271. scrollArea.setWidgetResizable(True)
  1272. scrollArea.setFrameStyle(0)
  1273. palette1 = scrollArea.palette()
  1274. palette1.setColor(QPalette.Background, Qt.transparent)
  1275. scrollArea.setPalette(palette1)
  1276. palette2 = scrollArea.palette()
  1277. palette2.setColor(QPalette.Background, palette2.color(QPalette.Button))
  1278. scrollAreaWidget = QWidget(scrollArea)
  1279. scrollAreaLayout = QVBoxLayout(scrollAreaWidget)
  1280. scrollAreaLayout.setSpacing(3)
  1281. expandBox = (len(paramList) < 50)
  1282. for paramInfo in paramList:
  1283. groupName = paramInfo['groupName']
  1284. if groupName:
  1285. groupSymbol, groupName = groupName.split(":",1)
  1286. groupLayout, groupWidget = groupWidgets.get(groupSymbol, (None, None))
  1287. if groupLayout is None:
  1288. groupWidget = CollapsibleBox(groupName, scrollAreaWidget, expandBox)
  1289. groupLayout = groupWidget.getContentLayout()
  1290. groupWidget.setPalette(palette2)
  1291. scrollAreaLayout.addWidget(groupWidget)
  1292. groupWidgets[groupSymbol] = (groupLayout, groupWidget)
  1293. else:
  1294. groupLayout = scrollAreaLayout
  1295. groupWidget = scrollAreaWidget
  1296. paramWidget = PluginParameter(groupWidget, self.host, paramInfo, self.fPluginId, tabIndex)
  1297. paramWidget.setLabelWidth(width)
  1298. groupLayout.addWidget(paramWidget)
  1299. self.fParameterList.append((paramType, paramInfo['index'], paramWidget))
  1300. if paramType == PARAMETER_INPUT:
  1301. paramWidget.valueChanged.connect(self.slot_parameterValueChanged)
  1302. paramWidget.mappedControlChanged.connect(self.slot_parameterMappedControlChanged)
  1303. paramWidget.mappedRangeChanged.connect(self.slot_parameterMappedRangeChanged)
  1304. paramWidget.midiChannelChanged.connect(self.slot_parameterMidiChannelChanged)
  1305. scrollAreaLayout.addStretch()
  1306. scrollArea.setWidget(scrollAreaWidget)
  1307. self.ui.tabWidget.addTab(scrollArea, tabPageName)
  1308. if paramType == PARAMETER_INPUT:
  1309. self.ui.tabWidget.setTabIcon(tabIndex, self.fTabIconOff)
  1310. self.fTabIconTimers.append(ICON_STATE_NULL)
  1311. def _updateCtrlPrograms(self):
  1312. self.ui.keyboard.setEnabled(self.fControlChannel >= 0)
  1313. if self.fPluginInfo['category'] != PLUGIN_CATEGORY_SYNTH:
  1314. return
  1315. if self.fPluginInfo['type'] not in (PLUGIN_INTERNAL, PLUGIN_SF2):
  1316. return
  1317. if self.fControlChannel < 0:
  1318. self.ui.cb_programs.setEnabled(False)
  1319. self.ui.cb_midi_programs.setEnabled(False)
  1320. return
  1321. self.ui.cb_programs.setEnabled(True)
  1322. self.ui.cb_midi_programs.setEnabled(True)
  1323. pIndex = self.host.get_current_program_index(self.fPluginId)
  1324. if self.ui.cb_programs.currentIndex() != pIndex:
  1325. self.setProgram(pIndex)
  1326. mpIndex = self.host.get_current_midi_program_index(self.fPluginId)
  1327. if self.ui.cb_midi_programs.currentIndex() != mpIndex:
  1328. self.setMidiProgram(mpIndex)
  1329. def _updateParameterValues(self):
  1330. for _, paramId, paramWidget in self.fParameterList:
  1331. paramWidget.blockSignals(True)
  1332. paramWidget.setValue(self.host.get_current_parameter_value(self.fPluginId, paramId))
  1333. paramWidget.blockSignals(False)
  1334. #------------------------------------------------------------------
  1335. # NOTE To speed things up, displayed data is not realtime. Reopen project to expose last changes.
  1336. def _createParameterXrayTab(self, tabName):
  1337. # How simple would be fit a value into cell? Yet to be as fast as we can.
  1338. def strFit(value):
  1339. if isinstance(value, str):
  1340. return value
  1341. # For 'carla-control', but anyway scalePoints are not work. #1984
  1342. elif isinstance(value, list):
  1343. return str(value) # It's []
  1344. elif abs(value) >= 1E8:
  1345. return '{:.3e}'.format(value)
  1346. elif value == int(value): # Zero falls here
  1347. return str(int(value))
  1348. else:
  1349. return strLim(value)
  1350. def strLineWrap(string, cut):
  1351. result = ''
  1352. while len(string) > cut: # FIXME Optimize me!
  1353. result += string[:cut] + '\n'
  1354. string = string[cut:]
  1355. result += string
  1356. return result
  1357. def addCell(section, name, string, toolTip = ''):
  1358. if x == table.columnCount():
  1359. table.insertColumn(x)
  1360. # jpka: FIXME Here we need vertical text. But impossible, no working examples.
  1361. # Only untested https://stackoverflow.com/questions/52162125/
  1362. nameWrapped = section + '\n' + strLineWrap(name, 6)
  1363. table.setHorizontalHeaderItem(x, QTableWidgetItem(nameWrapped))
  1364. table.horizontalHeader().setSectionResizeMode(x, QHeaderView.ResizeToContents)
  1365. if y == table.rowCount():
  1366. table.insertRow(y)
  1367. table.verticalHeader().setSectionResizeMode(y, QHeaderView.ResizeToContents)
  1368. item = QTableWidgetItem(string)
  1369. if toolTip:
  1370. item.setToolTip(toolTip)
  1371. table.setItem(y, x, item)
  1372. return
  1373. table = QTableWidget(self)
  1374. table.setObjectName("table")
  1375. table.setRowCount(1)
  1376. # table.setToolTipDuration(2000)
  1377. parameterCount = self.host.get_parameter_count(self.fPluginId)
  1378. if parameterCount <= 0:
  1379. return
  1380. y = 0
  1381. for i in range(parameterCount):
  1382. x = 0
  1383. param = self.host.get_parameter_data(self.fPluginId, i)
  1384. for name in param:
  1385. value = param[name]
  1386. if (name == 'type') and (value in (1, 2,)):
  1387. addCell('Data', name, str(value) + (' in',' out')[value - 1])
  1388. elif (name == 'hints'):
  1389. # toolTip = '<body>' + bin(value)[2:]
  1390. hints = ''
  1391. for bit in range(len(parameterHintsText)):
  1392. if (value & int(2**(bit-1))):
  1393. hint = parameterHintsText[bit-1]
  1394. # toolTip += '<br>' + hint
  1395. hints += ', ' + hint
  1396. addCell('Data', name, str(value)) # , toolTip + '</body>')
  1397. x += 1
  1398. addCell('Hints', '', hints[2:])
  1399. else:
  1400. addCell('Data', name, strFit(value))
  1401. x += 1
  1402. param = self.host.get_parameter_ranges(self.fPluginId, i)
  1403. for name in param:
  1404. addCell('Ranges', name, strFit(param[name]))
  1405. x += 1
  1406. param = self.host.get_parameter_info(self.fPluginId, i)
  1407. for name in param:
  1408. addCell('Info', name, strFit(param[name]))
  1409. x += 1
  1410. strScalePoints = ''
  1411. for j in range(param['scalePointCount']):
  1412. scalePointInfo = self.host.get_parameter_scalepoint_info(self.fPluginId, i, j)
  1413. strScalePoints += (strFit(scalePointInfo['value']) + ':' + scalePointInfo['label'] + ',')
  1414. if strScalePoints:
  1415. addCell('Scalepoint_info', 'Scale Points', strLineWrap(strScalePoints[:len(strScalePoints)-1], 80))
  1416. x += 1
  1417. y += 1
  1418. self.ui.tabWidget.addTab(table, tabName)
  1419. # self.ui.tabWidget.setToolTipDuration(2000)
  1420. #------------------------------------------------------------------
  1421. def _createDescriptionTab(self, tabPageName):
  1422. # jpka: To be filled from 'rdfs:comment'
  1423. strDescr = "To be filled from rdfs:comment"
  1424. realPluginName = self.host.get_real_plugin_name(self.fPluginId)
  1425. labelURI = self.fPluginInfo['label']
  1426. strLoadState = ""
  1427. programCount = self.host.get_program_count(self.fPluginId)
  1428. if programCount > 0:
  1429. strLoadState = '<div style="letter-spacing:1px"><br>'\
  1430. '<b>Note: </b>This plugin collected some presets for you.<br>'\
  1431. 'Use <i>Edit</i> tab, then <i>Load State</i> button.</div>'
  1432. scene = QGraphicsScene(self)
  1433. text = QGraphicsTextItem("",None)
  1434. text.setTextInteractionFlags(Qt.TextSelectableByMouse)
  1435. text.setTextWidth(600);
  1436. # text.setFont(QFont("Arial, 16")) # NOTE: All Qt sizes are in Pt; real px~=4/3Pt.
  1437. text.setHtml('<body>\
  1438. <h1>' + realPluginName + '</h1><br>'\
  1439. '<a href=' + labelURI + '>' + labelURI + '</a><br><br>'\
  1440. '<div style="line-height:1.5;">' + strDescr + '</div>' +\
  1441. strLoadState +\
  1442. '<body>');
  1443. scene.addItem(text)
  1444. view = QGraphicsView(scene, self)
  1445. self.ui.tabWidget.addTab(view, tabPageName)
  1446. #------------------------------------------------------------------
  1447. def testTimer(self):
  1448. self.fIdleTimerId = self.startTimer(50)
  1449. self.SIGTERM.connect(self.testTimerClose)
  1450. gCarla.gui = self
  1451. setUpSignals()
  1452. def testTimerClose(self):
  1453. self.close()
  1454. _app.quit()
  1455. #------------------------------------------------------------------
  1456. def closeEvent(self, event):
  1457. if self.fIdleTimerId != 0:
  1458. self.killTimer(self.fIdleTimerId)
  1459. self.fIdleTimerId = 0
  1460. self.host.engine_close()
  1461. QDialog.closeEvent(self, event)
  1462. def timerEvent(self, event):
  1463. if event.timerId() == self.fIdleTimerId:
  1464. self.host.engine_idle()
  1465. self.idleSlow()
  1466. QDialog.timerEvent(self, event)
  1467. # ------------------------------------------------------------------------------------------------------------
  1468. # Main
  1469. if __name__ == '__main__':
  1470. from carla_app import CarlaApplication
  1471. from carla_host import initHost as _initHost, loadHostSettings as _loadHostSettings
  1472. _app = CarlaApplication()
  1473. _host = _initHost("Widgets", None, False, False, False)
  1474. _loadHostSettings(_host)
  1475. _host.engine_init("JACK", "Carla-Widgets")
  1476. _host.add_plugin(BINARY_NATIVE, PLUGIN_DSSI, "/usr/lib/dssi/karplong.so", "karplong", "karplong", 0, None, 0x0)
  1477. _host.set_active(0, True)
  1478. gui2 = PluginEdit(None, _host, 0)
  1479. gui2.testTimer()
  1480. gui2.show()
  1481. _app.exit_exec()