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.

1831 lines
70KB

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