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.

1875 lines
72KB

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