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.

carla_patchbay.py 40KB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla patchbay widget code
  4. # Copyright (C) 2011-2014 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Config)
  19. from carla_config import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (Global)
  22. if config_UseQt5:
  23. from PyQt5.QtCore import QPointF, QTimer
  24. from PyQt5.QtGui import QImage
  25. from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
  26. from PyQt5.QtWidgets import QFrame, QGraphicsView, QGridLayout
  27. else:
  28. from PyQt4.QtCore import QPointF, QTimer
  29. from PyQt4.QtGui import QFrame, QGraphicsView, QGridLayout, QImage, QPrinter, QPrintDialog
  30. # ------------------------------------------------------------------------------------------------------------
  31. # Imports (Custom Stuff)
  32. import patchcanvas
  33. from carla_widgets import *
  34. from digitalpeakmeter import DigitalPeakMeter
  35. from pixmapkeyboard import PixmapKeyboardHArea
  36. # ------------------------------------------------------------------------------------------------------------
  37. # Try Import OpenGL
  38. try:
  39. if config_UseQt5:
  40. from PyQt5.QtOpenGL import QGLWidget
  41. else:
  42. from PyQt4.QtOpenGL import QGLWidget
  43. hasGL = True
  44. except:
  45. hasGL = False
  46. # ------------------------------------------------------------------------------------------------------------
  47. # Carla Canvas defaults
  48. CARLA_DEFAULT_CANVAS_SIZE_WIDTH = 3100
  49. CARLA_DEFAULT_CANVAS_SIZE_HEIGHT = 2400
  50. # ------------------------------------------------------------------------------------------------
  51. # Patchbay info class, used in main carla as replacement for PluginEdit
  52. class PluginInfo(object):
  53. def __init__(self, parent, pluginId):
  54. object.__init__(self)
  55. self.fGroupId = None
  56. self.fPluginId = pluginId
  57. self.fParameterList = [] # type, index, min, max
  58. self.reloadParameters()
  59. def close(self):
  60. for paramType, paramIndex, paramMin, paramMax in self.fParameterList:
  61. patchcanvas.removePort(self.fGroupId, -paramIndex-1)
  62. #------------------------------------------------------------------
  63. def reloadAll(self):
  64. self.reloadParameters()
  65. def reloadParameters(self):
  66. # TODO
  67. return
  68. # Remove all previous parameters
  69. self.close()
  70. hasGroup, groupId = patchcanvas.getPluginAsGroup(self.fPluginId)
  71. if not hasGroup:
  72. self.fGroupId = None
  73. return
  74. self.fGroupId = groupId
  75. # Reset
  76. self.fParameterList = []
  77. if gCarla.host is None:
  78. return
  79. parameterCount = gCarla.host.get_parameter_count(self.fPluginId)
  80. if parameterCount <= 0 or parameterCount > 25:
  81. return
  82. for i in range(parameterCount):
  83. paramInfo = gCarla.host.get_parameter_info(self.fPluginId, i)
  84. paramData = gCarla.host.get_parameter_data(self.fPluginId, i)
  85. paramRanges = gCarla.host.get_parameter_ranges(self.fPluginId, i)
  86. paramValue = gCarla.host.get_current_parameter_value(self.fPluginId, i)
  87. if paramData['type'] not in (PARAMETER_INPUT, PARAMETER_OUTPUT):
  88. #if paramData['type'] != PARAMETER_INPUT:
  89. continue
  90. if (paramData['hints'] & PARAMETER_IS_AUTOMABLE) == 0:
  91. continue
  92. portId = -i-1
  93. portMode = patchcanvas.PORT_MODE_OUTPUT if paramData['type'] == PARAMETER_OUTPUT else patchcanvas.PORT_MODE_INPUT
  94. portValue = (paramValue - paramRanges['min']) / (paramRanges['max'] - paramRanges['min'])
  95. patchcanvas.addPort(groupId, portId, paramInfo['name'], portMode, patchcanvas.PORT_TYPE_PARAMETER)
  96. patchcanvas.setPortValue(groupId, portId, portValue)
  97. self.fParameterList.append((paramData['type'], i, paramRanges['min'], paramRanges['max']))
  98. #------------------------------------------------------------------
  99. def setId(self, idx):
  100. self.fPluginId = idx
  101. def setParameterValue(self, parameterId, value):
  102. if self.fGroupId is None:
  103. return
  104. paramRanges = gCarla.host.get_parameter_ranges(self.fPluginId, parameterId)
  105. portValue = (value - paramRanges['min']) / (paramRanges['max'] - paramRanges['min'])
  106. patchcanvas.setPortValue(self.fGroupId, -parameterId-1, portValue)
  107. #------------------------------------------------------------------
  108. def idleSlow(self):
  109. # Update parameter outputs
  110. for paramType, paramIndex, paramMin, paramMax in self.fParameterList:
  111. if paramType == PARAMETER_OUTPUT:
  112. portValue = (gCarla.host.get_current_parameter_value(self.fPluginId, paramIndex) - paramMin) / (paramMax - paramMin)
  113. patchcanvas.setPortValue(self.fGroupId, -paramIndex-1, portValue)
  114. # ------------------------------------------------------------------------------------------------
  115. # Patchbay widget
  116. class CarlaPatchbayW(QFrame):
  117. def __init__(self, parent, doSetup = True, onlyPatchbay = True, is3D = False):
  118. QFrame.__init__(self, parent)
  119. self.fLayout = QGridLayout(self)
  120. self.fLayout.setContentsMargins(0, 0, 0, 0)
  121. self.fLayout.setSpacing(1)
  122. self.setLayout(self.fLayout)
  123. self.fView = QGraphicsView(self)
  124. self.fKeys = PixmapKeyboardHArea(self)
  125. self.fPeaksIn = DigitalPeakMeter(self)
  126. self.fPeaksOut = DigitalPeakMeter(self)
  127. self.fPeaksCleared = True
  128. self.fPeaksIn.setColor(DigitalPeakMeter.BLUE)
  129. self.fPeaksIn.setChannels(2)
  130. self.fPeaksIn.setOrientation(DigitalPeakMeter.VERTICAL)
  131. self.fPeaksIn.setFixedWidth(25)
  132. self.fPeaksOut.setColor(DigitalPeakMeter.GREEN)
  133. self.fPeaksOut.setChannels(2)
  134. self.fPeaksOut.setOrientation(DigitalPeakMeter.VERTICAL)
  135. self.fPeaksOut.setFixedWidth(25)
  136. self.fLayout.addWidget(self.fPeaksIn, 0, 0)
  137. self.fLayout.addWidget(self.fView, 0, 1) # self.fViewWidget if is3D else
  138. self.fLayout.addWidget(self.fPeaksOut, 0, 2)
  139. self.fLayout.addWidget(self.fKeys, 1, 0, 1, 0)
  140. # -------------------------------------------------------------
  141. # Internal stuff
  142. self.fParent = parent
  143. self.fPluginCount = 0
  144. self.fPluginList = []
  145. self.fIsOnlyPatchbay = onlyPatchbay
  146. self.fSelectedPlugins = []
  147. self.fCanvasWidth = 0
  148. self.fCanvasHeight = 0
  149. # -------------------------------------------------------------
  150. # Set-up Canvas Preview
  151. self.fMiniCanvasPreview = self.fParent.ui.miniCanvasPreview
  152. self.fMiniCanvasPreview.setRealParent(self)
  153. self.fMovingViaMiniCanvas = False
  154. # -------------------------------------------------------------
  155. # Set-up Canvas
  156. self.scene = patchcanvas.PatchScene(self, self.fView)
  157. self.fView.setScene(self.scene)
  158. self.fView.setRenderHint(QPainter.Antialiasing, bool(parent.fSavedSettings[CARLA_KEY_CANVAS_ANTIALIASING] == patchcanvas.ANTIALIASING_FULL))
  159. if parent.fSavedSettings[CARLA_KEY_CANVAS_USE_OPENGL] and hasGL: # and not is3D:
  160. self.fViewWidget = QGLWidget(self)
  161. self.fView.setViewport(self.fViewWidget)
  162. self.fView.setRenderHint(QPainter.HighQualityAntialiasing, parent.fSavedSettings[CARLA_KEY_CANVAS_HQ_ANTIALIASING])
  163. self.setupCanvas()
  164. QTimer.singleShot(100, self.slot_restoreScrollbarValues)
  165. # -------------------------------------------------------------
  166. # Connect actions to functions
  167. parent.ui.act_settings_show_meters.toggled.connect(self.slot_showCanvasMeters)
  168. parent.ui.act_settings_show_keyboard.toggled.connect(self.slot_showCanvasKeyboard)
  169. self.fView.horizontalScrollBar().valueChanged.connect(self.slot_horizontalScrollBarChanged)
  170. self.fView.verticalScrollBar().valueChanged.connect(self.slot_verticalScrollBarChanged)
  171. self.scene.scaleChanged.connect(self.slot_canvasScaleChanged)
  172. self.scene.sceneGroupMoved.connect(self.slot_canvasItemMoved)
  173. self.scene.pluginSelected.connect(self.slot_canvasPluginSelected)
  174. self.fMiniCanvasPreview.miniCanvasMoved.connect(self.slot_miniCanvasMoved)
  175. self.fKeys.keyboard.noteOn.connect(self.slot_noteOn)
  176. self.fKeys.keyboard.noteOff.connect(self.slot_noteOff)
  177. # -------------------------------------------------------------
  178. # Load Settings
  179. settings = QSettings()
  180. showMeters = settings.value("ShowMeters", False, type=bool)
  181. self.fParent.ui.act_settings_show_meters.setChecked(showMeters)
  182. self.fPeaksIn.setVisible(showMeters)
  183. self.fPeaksOut.setVisible(showMeters)
  184. showKeyboard = settings.value("ShowKeyboard", True, type=bool)
  185. self.fParent.ui.act_settings_show_keyboard.setChecked(showKeyboard)
  186. self.fKeys.setVisible(showKeyboard)
  187. # -------------------------------------------------------------
  188. # Connect actions to functions (part 2)
  189. if not doSetup: return
  190. parent.ui.act_plugins_enable.triggered.connect(self.slot_pluginsEnable)
  191. parent.ui.act_plugins_disable.triggered.connect(self.slot_pluginsDisable)
  192. parent.ui.act_plugins_volume100.triggered.connect(self.slot_pluginsVolume100)
  193. parent.ui.act_plugins_mute.triggered.connect(self.slot_pluginsMute)
  194. parent.ui.act_plugins_wet100.triggered.connect(self.slot_pluginsWet100)
  195. parent.ui.act_plugins_bypass.triggered.connect(self.slot_pluginsBypass)
  196. parent.ui.act_plugins_center.triggered.connect(self.slot_pluginsCenter)
  197. parent.ui.act_plugins_panic.triggered.connect(self.slot_pluginsDisable)
  198. parent.ui.act_canvas_arrange.setEnabled(False) # TODO, later
  199. parent.ui.act_canvas_arrange.triggered.connect(self.slot_canvasArrange)
  200. parent.ui.act_canvas_refresh.triggered.connect(self.slot_canvasRefresh)
  201. parent.ui.act_canvas_zoom_fit.triggered.connect(self.slot_canvasZoomFit)
  202. parent.ui.act_canvas_zoom_in.triggered.connect(self.slot_canvasZoomIn)
  203. parent.ui.act_canvas_zoom_out.triggered.connect(self.slot_canvasZoomOut)
  204. parent.ui.act_canvas_zoom_100.triggered.connect(self.slot_canvasZoomReset)
  205. parent.ui.act_canvas_print.triggered.connect(self.slot_canvasPrint)
  206. parent.ui.act_canvas_save_image.triggered.connect(self.slot_canvasSaveImage)
  207. parent.ui.act_settings_configure.triggered.connect(self.slot_configureCarla)
  208. parent.ParameterValueChangedCallback.connect(self.slot_handleParameterValueChangedCallback)
  209. parent.ParameterDefaultChangedCallback.connect(self.slot_handleParameterDefaultChangedCallback)
  210. parent.ParameterMidiChannelChangedCallback.connect(self.slot_handleParameterMidiChannelChangedCallback)
  211. parent.ParameterMidiCcChangedCallback.connect(self.slot_handleParameterMidiCcChangedCallback)
  212. parent.ProgramChangedCallback.connect(self.slot_handleProgramChangedCallback)
  213. parent.MidiProgramChangedCallback.connect(self.slot_handleMidiProgramChangedCallback)
  214. parent.NoteOnCallback.connect(self.slot_handleNoteOnCallback)
  215. parent.NoteOffCallback.connect(self.slot_handleNoteOffCallback)
  216. parent.UpdateCallback.connect(self.slot_handleUpdateCallback)
  217. parent.ReloadInfoCallback.connect(self.slot_handleReloadInfoCallback)
  218. parent.ReloadParametersCallback.connect(self.slot_handleReloadParametersCallback)
  219. parent.ReloadProgramsCallback.connect(self.slot_handleReloadProgramsCallback)
  220. parent.ReloadAllCallback.connect(self.slot_handleReloadAllCallback)
  221. parent.PatchbayClientAddedCallback.connect(self.slot_handlePatchbayClientAddedCallback)
  222. parent.PatchbayClientRemovedCallback.connect(self.slot_handlePatchbayClientRemovedCallback)
  223. parent.PatchbayClientRenamedCallback.connect(self.slot_handlePatchbayClientRenamedCallback)
  224. parent.PatchbayClientDataChangedCallback.connect(self.slot_handlePatchbayClientDataChangedCallback)
  225. parent.PatchbayPortAddedCallback.connect(self.slot_handlePatchbayPortAddedCallback)
  226. parent.PatchbayPortRemovedCallback.connect(self.slot_handlePatchbayPortRemovedCallback)
  227. parent.PatchbayPortRenamedCallback.connect(self.slot_handlePatchbayPortRenamedCallback)
  228. parent.PatchbayPortValueChangedCallback.connect(self.slot_handlePatchbayPortValueChangedCallback)
  229. parent.PatchbayConnectionAddedCallback.connect(self.slot_handlePatchbayConnectionAddedCallback)
  230. parent.PatchbayConnectionRemovedCallback.connect(self.slot_handlePatchbayConnectionRemovedCallback)
  231. # -----------------------------------------------------------------
  232. def getPluginCount(self):
  233. return self.fPluginCount
  234. # -----------------------------------------------------------------
  235. def addPlugin(self, pluginId, isProjectLoading):
  236. if self.fIsOnlyPatchbay:
  237. pitem = PluginEdit(self, pluginId)
  238. else:
  239. pitem = PluginInfo(self, pluginId)
  240. self.fPluginList.append(pitem)
  241. self.fPluginCount += 1
  242. if self.fIsOnlyPatchbay and not isProjectLoading:
  243. gCarla.host.set_active(pluginId, True)
  244. def removePlugin(self, pluginId):
  245. patchcanvas.handlePluginRemoved(pluginId)
  246. if pluginId in self.fSelectedPlugins:
  247. self.clearSideStuff()
  248. if pluginId >= self.fPluginCount:
  249. return
  250. pitem = self.fPluginList[pluginId]
  251. if pitem is None:
  252. return
  253. self.fPluginCount -= 1
  254. self.fPluginList.pop(pluginId)
  255. pitem.close()
  256. del pitem
  257. # push all plugins 1 slot back
  258. for i in range(pluginId, self.fPluginCount):
  259. pitem = self.fPluginList[i]
  260. pitem.setId(i)
  261. def renamePlugin(self, pluginId, newName):
  262. if pluginId >= self.fPluginCount:
  263. return
  264. pitem = self.fPluginList[pluginId]
  265. if pitem is None:
  266. return
  267. pitem.setName(newName)
  268. def disablePlugin(self, pluginId, errorMsg):
  269. if pluginId >= self.fPluginCount:
  270. return
  271. pitem = self.fPluginList[pluginId]
  272. if pitem is None:
  273. return
  274. def removeAllPlugins(self):
  275. for pitem in self.fPluginList:
  276. if pitem is None:
  277. break
  278. pitem.close()
  279. del pitem
  280. self.fPluginCount = 0
  281. self.fPluginList = []
  282. self.clearSideStuff()
  283. patchcanvas.handlePluginRemoved(0)
  284. # -----------------------------------------------------------------
  285. def engineStarted(self):
  286. pass
  287. def engineStopped(self):
  288. patchcanvas.clear()
  289. def engineChanged(self):
  290. pass
  291. # -----------------------------------------------------------------
  292. def idleFast(self):
  293. if self.fPluginCount == 0:
  294. return
  295. for pluginId in self.fSelectedPlugins:
  296. self.fPeaksCleared = False
  297. if self.fPeaksIn.isVisible():
  298. self.fPeaksIn.displayMeter(1, gCarla.host.get_input_peak_value(pluginId, True))
  299. self.fPeaksIn.displayMeter(2, gCarla.host.get_input_peak_value(pluginId, False))
  300. if self.fPeaksOut.isVisible():
  301. self.fPeaksOut.displayMeter(1, gCarla.host.get_output_peak_value(pluginId, True))
  302. self.fPeaksOut.displayMeter(2, gCarla.host.get_output_peak_value(pluginId, False))
  303. return
  304. if self.fPeaksCleared:
  305. return
  306. self.fPeaksCleared = True
  307. self.fPeaksIn.displayMeter(1, 0.0, True)
  308. self.fPeaksIn.displayMeter(2, 0.0, True)
  309. self.fPeaksOut.displayMeter(1, 0.0, True)
  310. self.fPeaksOut.displayMeter(2, 0.0, True)
  311. def idleSlow(self):
  312. for pitem in self.fPluginList:
  313. if pitem is None:
  314. break
  315. pitem.idleSlow()
  316. # -----------------------------------------------------------------
  317. def projectLoadingStarted(self):
  318. pass
  319. def projectLoadingFinished(self):
  320. QTimer.singleShot(1000, self.slot_canvasRefresh)
  321. # -----------------------------------------------------------------
  322. def saveSettings(self, settings):
  323. settings.setValue("ShowMeters", self.fParent.ui.act_settings_show_meters.isChecked())
  324. settings.setValue("ShowKeyboard", self.fParent.ui.act_settings_show_keyboard.isChecked())
  325. settings.setValue("HorizontalScrollBarValue", self.fView.horizontalScrollBar().value())
  326. settings.setValue("VerticalScrollBarValue", self.fView.verticalScrollBar().value())
  327. def showEditDialog(self, pluginId):
  328. if pluginId >= self.fPluginCount:
  329. return
  330. pitem = self.fPluginList[pluginId]
  331. if pitem is None:
  332. return
  333. pitem.show()
  334. # -----------------------------------------------------------------
  335. # called by PluginEdit to plugin skin parent, ignored here
  336. def editDialogChanged(self, visible):
  337. pass
  338. def recheckPluginHints(self, hints):
  339. pass
  340. # -----------------------------------------------------------------
  341. def clearSideStuff(self):
  342. self.scene.clearSelection()
  343. self.fSelectedPlugins = []
  344. self.fKeys.keyboard.allNotesOff(False)
  345. self.fKeys.setEnabled(False)
  346. self.fPeaksCleared = True
  347. self.fPeaksIn.displayMeter(1, 0.0, True)
  348. self.fPeaksIn.displayMeter(2, 0.0, True)
  349. self.fPeaksOut.displayMeter(1, 0.0, True)
  350. self.fPeaksOut.displayMeter(2, 0.0, True)
  351. def setupCanvas(self):
  352. pOptions = patchcanvas.options_t()
  353. pOptions.theme_name = self.fParent.fSavedSettings[CARLA_KEY_CANVAS_THEME]
  354. pOptions.auto_hide_groups = self.fParent.fSavedSettings[CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS]
  355. pOptions.use_bezier_lines = self.fParent.fSavedSettings[CARLA_KEY_CANVAS_USE_BEZIER_LINES]
  356. pOptions.antialiasing = self.fParent.fSavedSettings[CARLA_KEY_CANVAS_ANTIALIASING]
  357. pOptions.eyecandy = self.fParent.fSavedSettings[CARLA_KEY_CANVAS_EYE_CANDY]
  358. pFeatures = patchcanvas.features_t()
  359. pFeatures.group_info = False
  360. pFeatures.group_rename = False
  361. pFeatures.port_info = False
  362. pFeatures.port_rename = False
  363. pFeatures.handle_group_pos = True
  364. patchcanvas.setOptions(pOptions)
  365. patchcanvas.setFeatures(pFeatures)
  366. patchcanvas.init("Carla2", self.scene, canvasCallback, False)
  367. tryCanvasSize = self.fParent.fSavedSettings[CARLA_KEY_CANVAS_SIZE].split("x")
  368. if len(tryCanvasSize) == 2 and tryCanvasSize[0].isdigit() and tryCanvasSize[1].isdigit():
  369. self.fCanvasWidth = int(tryCanvasSize[0])
  370. self.fCanvasHeight = int(tryCanvasSize[1])
  371. else:
  372. self.fCanvasWidth = CARLA_DEFAULT_CANVAS_SIZE_WIDTH
  373. self.fCanvasHeight = CARLA_DEFAULT_CANVAS_SIZE_HEIGHT
  374. patchcanvas.setCanvasSize(0, 0, self.fCanvasWidth, self.fCanvasHeight)
  375. patchcanvas.setInitialPos(self.fCanvasWidth / 2, self.fCanvasHeight / 2)
  376. self.fView.setSceneRect(0, 0, self.fCanvasWidth, self.fCanvasHeight)
  377. self.themeData = [self.fCanvasWidth, self.fCanvasHeight, patchcanvas.canvas.theme.canvas_bg, patchcanvas.canvas.theme.rubberband_brush, patchcanvas.canvas.theme.rubberband_pen.color()]
  378. def updateCanvasInitialPos(self):
  379. x = self.fView.horizontalScrollBar().value() + self.width()/4
  380. y = self.fView.verticalScrollBar().value() + self.height()/4
  381. patchcanvas.setInitialPos(x, y)
  382. # -----------------------------------------------------------------
  383. @pyqtSlot(bool)
  384. def slot_showCanvasMeters(self, yesNo):
  385. self.fPeaksIn.setVisible(yesNo)
  386. self.fPeaksOut.setVisible(yesNo)
  387. @pyqtSlot(bool)
  388. def slot_showCanvasKeyboard(self, yesNo):
  389. self.fKeys.setVisible(yesNo)
  390. # -----------------------------------------------------------------
  391. @pyqtSlot()
  392. def slot_miniCanvasCheckAll(self):
  393. self.slot_miniCanvasCheckSize()
  394. self.slot_horizontalScrollBarChanged(self.fView.horizontalScrollBar().value())
  395. self.slot_verticalScrollBarChanged(self.fView.verticalScrollBar().value())
  396. @pyqtSlot()
  397. def slot_miniCanvasCheckSize(self):
  398. self.fMiniCanvasPreview.setViewSize(float(self.width()) / self.fCanvasWidth, float(self.height()) / self.fCanvasHeight)
  399. @pyqtSlot(int)
  400. def slot_horizontalScrollBarChanged(self, value):
  401. if self.fMovingViaMiniCanvas: return
  402. maximum = self.fView.horizontalScrollBar().maximum()
  403. if maximum == 0:
  404. xp = 0
  405. else:
  406. xp = float(value) / maximum
  407. self.fMiniCanvasPreview.setViewPosX(xp)
  408. self.updateCanvasInitialPos()
  409. @pyqtSlot(int)
  410. def slot_verticalScrollBarChanged(self, value):
  411. if self.fMovingViaMiniCanvas: return
  412. maximum = self.fView.verticalScrollBar().maximum()
  413. if maximum == 0:
  414. yp = 0
  415. else:
  416. yp = float(value) / maximum
  417. self.fMiniCanvasPreview.setViewPosY(yp)
  418. self.updateCanvasInitialPos()
  419. @pyqtSlot()
  420. def slot_restoreScrollbarValues(self):
  421. settings = QSettings()
  422. self.fView.horizontalScrollBar().setValue(settings.value("HorizontalScrollBarValue", self.fView.horizontalScrollBar().maximum()/2, type=int))
  423. self.fView.verticalScrollBar().setValue(settings.value("VerticalScrollBarValue", self.fView.verticalScrollBar().maximum()/2, type=int))
  424. # -----------------------------------------------------------------
  425. @pyqtSlot(float)
  426. def slot_canvasScaleChanged(self, scale):
  427. self.fMiniCanvasPreview.setViewScale(scale)
  428. @pyqtSlot(int, int, QPointF)
  429. def slot_canvasItemMoved(self, group_id, split_mode, pos):
  430. self.fMiniCanvasPreview.update()
  431. @pyqtSlot(list)
  432. def slot_canvasPluginSelected(self, pluginList):
  433. self.fKeys.keyboard.allNotesOff(False)
  434. self.fKeys.setEnabled(len(pluginList) != 0) # and self.fPluginCount > 0
  435. self.fSelectedPlugins = pluginList
  436. @pyqtSlot(float, float)
  437. def slot_miniCanvasMoved(self, xp, yp):
  438. self.fMovingViaMiniCanvas = True
  439. self.fView.horizontalScrollBar().setValue(xp * self.fView.horizontalScrollBar().maximum())
  440. self.fView.verticalScrollBar().setValue(yp * self.fView.verticalScrollBar().maximum())
  441. self.fMovingViaMiniCanvas = False
  442. self.updateCanvasInitialPos()
  443. # -----------------------------------------------------------------
  444. @pyqtSlot(int)
  445. def slot_noteOn(self, note):
  446. for pluginId in self.fSelectedPlugins:
  447. gCarla.host.send_midi_note(pluginId, 0, note, 100)
  448. @pyqtSlot(int)
  449. def slot_noteOff(self, note):
  450. for pluginId in self.fSelectedPlugins:
  451. gCarla.host.send_midi_note(pluginId, 0, note, 0)
  452. # -----------------------------------------------------------------
  453. @pyqtSlot()
  454. def slot_pluginsEnable(self):
  455. if not gCarla.host.is_engine_running():
  456. return
  457. for i in range(self.fPluginCount):
  458. gCarla.host.set_active(i, True)
  459. @pyqtSlot()
  460. def slot_pluginsDisable(self):
  461. if not gCarla.host.is_engine_running():
  462. return
  463. for i in range(self.fPluginCount):
  464. gCarla.host.set_active(i, False)
  465. @pyqtSlot()
  466. def slot_pluginsVolume100(self):
  467. if not gCarla.host.is_engine_running():
  468. return
  469. for i in range(self.fPluginCount):
  470. pitem = self.fPluginList[i]
  471. if pitem is None:
  472. break
  473. if pitem.getHints() & PLUGIN_CAN_VOLUME:
  474. pitem.setParameterValue(PARAMETER_VOLUME, 1.0)
  475. gCarla.host.set_volume(i, 1.0)
  476. @pyqtSlot()
  477. def slot_pluginsMute(self):
  478. if not gCarla.host.is_engine_running():
  479. return
  480. for i in range(self.fPluginCount):
  481. pitem = self.fPluginList[i]
  482. if pitem is None:
  483. break
  484. if pitem.getHints() & PLUGIN_CAN_VOLUME:
  485. pitem.setParameterValue(PARAMETER_VOLUME, 0.0)
  486. gCarla.host.set_volume(i, 0.0)
  487. @pyqtSlot()
  488. def slot_pluginsWet100(self):
  489. if not gCarla.host.is_engine_running():
  490. return
  491. for i in range(self.fPluginCount):
  492. pitem = self.fPluginList[i]
  493. if pitem is None:
  494. break
  495. if pitem.getHints() & PLUGIN_CAN_DRYWET:
  496. pitem.setParameterValue(PARAMETER_DRYWET, 1.0)
  497. gCarla.host.set_drywet(i, 1.0)
  498. @pyqtSlot()
  499. def slot_pluginsBypass(self):
  500. if not gCarla.host.is_engine_running():
  501. return
  502. for i in range(self.fPluginCount):
  503. pitem = self.fPluginList[i]
  504. if pitem is None:
  505. break
  506. if pitem.getHints() & PLUGIN_CAN_DRYWET:
  507. pitem.setParameterValue(PARAMETER_DRYWET, 0.0)
  508. gCarla.host.set_drywet(i, 0.0)
  509. @pyqtSlot()
  510. def slot_pluginsCenter(self):
  511. if not gCarla.host.is_engine_running():
  512. return
  513. for i in range(self.fPluginCount):
  514. pitem = self.fPluginList[i]
  515. if pitem is None:
  516. break
  517. if pitem.getHints() & PLUGIN_CAN_BALANCE:
  518. pitem.setParameterValue(PARAMETER_BALANCE_LEFT, -1.0)
  519. pitem.setParameterValue(PARAMETER_BALANCE_RIGHT, 1.0)
  520. gCarla.host.set_balance_left(i, -1.0)
  521. gCarla.host.set_balance_right(i, 1.0)
  522. if pitem.getHints() & PLUGIN_CAN_PANNING:
  523. pitem.setParameterValue(PARAMETER_PANNING, 0.0)
  524. gCarla.host.set_panning(i, 0.0)
  525. # -----------------------------------------------------------------
  526. @pyqtSlot()
  527. def slot_configureCarla(self):
  528. if self.fParent is None or not self.fParent.openSettingsWindow(True, hasGL):
  529. return
  530. self.fParent.loadSettings(False)
  531. patchcanvas.clear()
  532. self.setupCanvas()
  533. self.fParent.updateContainer(self.themeData)
  534. self.slot_miniCanvasCheckAll()
  535. if gCarla.host.is_engine_running():
  536. gCarla.host.patchbay_refresh()
  537. # -----------------------------------------------------------------
  538. @pyqtSlot(int, int, float)
  539. def slot_handleParameterValueChangedCallback(self, pluginId, index, value):
  540. if pluginId >= self.fPluginCount:
  541. return
  542. pitem = self.fPluginList[pluginId]
  543. if pitem is None:
  544. return
  545. pitem.setParameterValue(index, value)
  546. @pyqtSlot(int, int, float)
  547. def slot_handleParameterDefaultChangedCallback(self, pluginId, index, value):
  548. if pluginId >= self.fPluginCount:
  549. return
  550. pitem = self.fPluginList[pluginId]
  551. if pitem is None:
  552. return
  553. pitem.setParameterDefault(index, value)
  554. @pyqtSlot(int, int, int)
  555. def slot_handleParameterMidiCcChangedCallback(self, pluginId, index, cc):
  556. if pluginId >= self.fPluginCount:
  557. return
  558. pitem = self.fPluginList[pluginId]
  559. if pitem is None:
  560. return
  561. pitem.setParameterMidiControl(index, cc)
  562. @pyqtSlot(int, int, int)
  563. def slot_handleParameterMidiChannelChangedCallback(self, pluginId, index, channel):
  564. if pluginId >= self.fPluginCount:
  565. return
  566. pitem = self.fPluginList[pluginId]
  567. if pitem is None:
  568. return
  569. pitem.setParameterMidiChannel(index, channel)
  570. # -----------------------------------------------------------------
  571. @pyqtSlot(int, int)
  572. def slot_handleProgramChangedCallback(self, pluginId, index):
  573. if pluginId >= self.fPluginCount:
  574. return
  575. pitem = self.fPluginList[pluginId]
  576. if pitem is None:
  577. return
  578. pitem.setProgram(index)
  579. @pyqtSlot(int, int)
  580. def slot_handleMidiProgramChangedCallback(self, pluginId, index):
  581. if pluginId >= self.fPluginCount:
  582. return
  583. pitem = self.fPluginList[pluginId]
  584. if pitem is None:
  585. return
  586. pitem.setMidiProgram(index)
  587. # -----------------------------------------------------------------
  588. @pyqtSlot(int, int, int, int)
  589. def slot_handleNoteOnCallback(self, pluginId, channel, note, velo):
  590. if pluginId in self.fSelectedPlugins:
  591. self.fKeys.keyboard.sendNoteOn(note, False)
  592. if not self.fIsOnlyPatchbay:
  593. return
  594. if pluginId >= self.fPluginCount:
  595. return
  596. pitem = self.fPluginList[pluginId]
  597. if pitem is None:
  598. return
  599. pitem.sendNoteOn(channel, note)
  600. @pyqtSlot(int, int, int)
  601. def slot_handleNoteOffCallback(self, pluginId, channel, note):
  602. if pluginId in self.fSelectedPlugins:
  603. self.fKeys.keyboard.sendNoteOff(note, False)
  604. if not self.fIsOnlyPatchbay:
  605. return
  606. if pluginId >= self.fPluginCount:
  607. return
  608. pitem = self.fPluginList[pluginId]
  609. if pitem is None:
  610. return
  611. pitem.sendNoteOff(channel, note)
  612. # -----------------------------------------------------------------
  613. @pyqtSlot(int)
  614. def slot_handleUpdateCallback(self, pluginId):
  615. if pluginId >= self.fPluginCount:
  616. return
  617. pitem = self.fPluginList[pluginId]
  618. if pitem is None:
  619. return
  620. pitem.updateInfo()
  621. @pyqtSlot(int)
  622. def slot_handleReloadInfoCallback(self, pluginId):
  623. if pluginId >= self.fPluginCount:
  624. return
  625. pitem = self.fPluginList[pluginId]
  626. if pitem is None:
  627. return
  628. pitem.reloadInfo()
  629. @pyqtSlot(int)
  630. def slot_handleReloadParametersCallback(self, pluginId):
  631. if pluginId >= self.fPluginCount:
  632. return
  633. pitem = self.fPluginList[pluginId]
  634. if pitem is None:
  635. return
  636. pitem.reloadParameters()
  637. @pyqtSlot(int)
  638. def slot_handleReloadProgramsCallback(self, pluginId):
  639. if pluginId >= self.fPluginCount:
  640. return
  641. pitem = self.fPluginList[pluginId]
  642. if pitem is None:
  643. return
  644. pitem.reloadPrograms()
  645. @pyqtSlot(int)
  646. def slot_handleReloadAllCallback(self, pluginId):
  647. if pluginId >= self.fPluginCount:
  648. return
  649. pitem = self.fPluginList[pluginId]
  650. if pitem is None:
  651. return
  652. pitem.reloadAll()
  653. # -----------------------------------------------------------------
  654. @pyqtSlot(int, int, int, str)
  655. def slot_handlePatchbayClientAddedCallback(self, clientId, clientIcon, pluginId, clientName):
  656. pcSplit = patchcanvas.SPLIT_UNDEF
  657. pcIcon = patchcanvas.ICON_APPLICATION
  658. if clientIcon == PATCHBAY_ICON_PLUGIN:
  659. pcIcon = patchcanvas.ICON_PLUGIN
  660. if clientIcon == PATCHBAY_ICON_HARDWARE:
  661. pcIcon = patchcanvas.ICON_HARDWARE
  662. elif clientIcon == PATCHBAY_ICON_CARLA:
  663. pass
  664. elif clientIcon == PATCHBAY_ICON_DISTRHO:
  665. pcIcon = patchcanvas.ICON_DISTRHO
  666. elif clientIcon == PATCHBAY_ICON_FILE:
  667. pcIcon = patchcanvas.ICON_FILE
  668. patchcanvas.addGroup(clientId, clientName, pcSplit, pcIcon)
  669. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  670. if pluginId < 0:
  671. return
  672. if pluginId >= self.fPluginCount:
  673. print("sorry, can't map this plugin to canvas client", pluginId, self.fPluginCount)
  674. return
  675. patchcanvas.setGroupAsPlugin(clientId, pluginId, bool(gCarla.host.get_plugin_info(pluginId)['hints'] & PLUGIN_HAS_CUSTOM_UI))
  676. @pyqtSlot(int)
  677. def slot_handlePatchbayClientRemovedCallback(self, clientId):
  678. #if not self.fEngineStarted: return
  679. patchcanvas.removeGroup(clientId)
  680. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  681. @pyqtSlot(int, str)
  682. def slot_handlePatchbayClientRenamedCallback(self, clientId, newClientName):
  683. patchcanvas.renameGroup(clientId, newClientName)
  684. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  685. @pyqtSlot(int, int, int)
  686. def slot_handlePatchbayClientDataChangedCallback(self, clientId, clientIcon, pluginId):
  687. pcIcon = patchcanvas.ICON_APPLICATION
  688. if clientIcon == PATCHBAY_ICON_PLUGIN:
  689. pcIcon = patchcanvas.ICON_PLUGIN
  690. if clientIcon == PATCHBAY_ICON_HARDWARE:
  691. pcIcon = patchcanvas.ICON_HARDWARE
  692. elif clientIcon == PATCHBAY_ICON_CARLA:
  693. pass
  694. elif clientIcon == PATCHBAY_ICON_DISTRHO:
  695. pcIcon = patchcanvas.ICON_DISTRHO
  696. elif clientIcon == PATCHBAY_ICON_FILE:
  697. pcIcon = patchcanvas.ICON_FILE
  698. patchcanvas.setGroupIcon(clientId, pcIcon)
  699. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  700. if pluginId < 0:
  701. return
  702. if pluginId >= self.fPluginCount:
  703. print("sorry, can't map this plugin to canvas client", pluginId, self.getPluginCount())
  704. return
  705. patchcanvas.setGroupAsPlugin(clientId, pluginId, bool(gCarla.host.get_plugin_info(pluginId)['hints'] & PLUGIN_HAS_CUSTOM_UI))
  706. @pyqtSlot(int, int, int, str)
  707. def slot_handlePatchbayPortAddedCallback(self, clientId, portId, portFlags, portName):
  708. isAlternate = False
  709. if (portFlags & PATCHBAY_PORT_IS_INPUT):
  710. portMode = patchcanvas.PORT_MODE_INPUT
  711. else:
  712. portMode = patchcanvas.PORT_MODE_OUTPUT
  713. if (portFlags & PATCHBAY_PORT_TYPE_AUDIO):
  714. portType = patchcanvas.PORT_TYPE_AUDIO_JACK
  715. elif (portFlags & PATCHBAY_PORT_TYPE_CV):
  716. isAlternate = True
  717. portType = patchcanvas.PORT_TYPE_AUDIO_JACK
  718. elif (portFlags & PATCHBAY_PORT_TYPE_MIDI):
  719. portType = patchcanvas.PORT_TYPE_MIDI_JACK
  720. elif (portFlags & PATCHBAY_PORT_TYPE_PARAMETER):
  721. portType = patchcanvas.PORT_TYPE_PARAMETER
  722. else:
  723. portType = patchcanvas.PORT_TYPE_NULL
  724. patchcanvas.addPort(clientId, portId, portName, portMode, portType, isAlternate)
  725. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  726. @pyqtSlot(int, int)
  727. def slot_handlePatchbayPortRemovedCallback(self, groupId, portId):
  728. #if not self.fEngineStarted: return
  729. patchcanvas.removePort(groupId, portId)
  730. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  731. @pyqtSlot(int, int, str)
  732. def slot_handlePatchbayPortRenamedCallback(self, groupId, portId, newPortName):
  733. patchcanvas.renamePort(groupId, portId, newPortName)
  734. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  735. @pyqtSlot(int, int, float)
  736. def slot_handlePatchbayPortValueChangedCallback(self, groupId, portId, value):
  737. patchcanvas.setPortValue(groupId, portId, value)
  738. @pyqtSlot(int, int, int, int, int)
  739. def slot_handlePatchbayConnectionAddedCallback(self, connectionId, groupOutId, portOutId, groupInId, portInId):
  740. patchcanvas.connectPorts(connectionId, groupOutId, portOutId, groupInId, portInId)
  741. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  742. @pyqtSlot(int, int, int)
  743. def slot_handlePatchbayConnectionRemovedCallback(self, connectionId, portOutId, portInId):
  744. #if not self.fEngineStarted: return
  745. patchcanvas.disconnectPorts(connectionId)
  746. QTimer.singleShot(0, self.fMiniCanvasPreview.update)
  747. # -----------------------------------------------------------------
  748. @pyqtSlot()
  749. def slot_canvasArrange(self):
  750. patchcanvas.arrange()
  751. @pyqtSlot()
  752. def slot_canvasRefresh(self):
  753. patchcanvas.clear()
  754. if gCarla.host.is_engine_running():
  755. gCarla.host.patchbay_refresh()
  756. for pitem in self.fPluginList:
  757. if pitem is None:
  758. break
  759. pitem.reloadAll()
  760. QTimer.singleShot(1000 if self.fParent.fSavedSettings[CARLA_KEY_CANVAS_EYE_CANDY] else 0, self.fMiniCanvasPreview.update)
  761. @pyqtSlot()
  762. def slot_canvasZoomFit(self):
  763. self.scene.zoom_fit()
  764. @pyqtSlot()
  765. def slot_canvasZoomIn(self):
  766. self.scene.zoom_in()
  767. @pyqtSlot()
  768. def slot_canvasZoomOut(self):
  769. self.scene.zoom_out()
  770. @pyqtSlot()
  771. def slot_canvasZoomReset(self):
  772. self.scene.zoom_reset()
  773. @pyqtSlot()
  774. def slot_canvasPrint(self):
  775. self.scene.clearSelection()
  776. self.fExportPrinter = QPrinter()
  777. dialog = QPrintDialog(self.fExportPrinter, self)
  778. if dialog.exec_():
  779. painter = QPainter(self.fExportPrinter)
  780. painter.save()
  781. painter.setRenderHint(QPainter.Antialiasing)
  782. painter.setRenderHint(QPainter.TextAntialiasing)
  783. self.scene.render(painter)
  784. painter.restore()
  785. @pyqtSlot()
  786. def slot_canvasSaveImage(self):
  787. newPath = QFileDialog.getSaveFileName(self, self.tr("Save Image"), filter=self.tr("PNG Image (*.png);;JPEG Image (*.jpg)"))
  788. if newPath:
  789. self.scene.clearSelection()
  790. if newPath.lower().endswith((".jpg",)):
  791. imgFormat = "JPG"
  792. elif newPath.lower().endswith((".png",)):
  793. imgFormat = "PNG"
  794. else:
  795. # File-dialog may not auto-add the extension
  796. imgFormat = "PNG"
  797. newPath += ".png"
  798. self.fExportImage = QImage(self.scene.sceneRect().width(), self.scene.sceneRect().height(), QImage.Format_RGB32)
  799. painter = QPainter(self.fExportImage)
  800. painter.save()
  801. painter.setRenderHint(QPainter.Antialiasing) # TODO - set true, cleanup this
  802. painter.setRenderHint(QPainter.TextAntialiasing)
  803. self.scene.render(painter)
  804. self.fExportImage.save(newPath, imgFormat, 100)
  805. painter.restore()
  806. # -----------------------------------------------------------------
  807. def resizeEvent(self, event):
  808. QFrame.resizeEvent(self, event)
  809. self.slot_miniCanvasCheckSize()
  810. # ------------------------------------------------------------------------------------------------
  811. # Canvas callback
  812. def canvasCallback(action, value1, value2, valueStr):
  813. if action == patchcanvas.ACTION_GROUP_INFO:
  814. pass
  815. elif action == patchcanvas.ACTION_GROUP_RENAME:
  816. pass
  817. elif action == patchcanvas.ACTION_GROUP_SPLIT:
  818. groupId = value1
  819. patchcanvas.splitGroup(groupId)
  820. gCarla.gui.ui.miniCanvasPreview.update()
  821. elif action == patchcanvas.ACTION_GROUP_JOIN:
  822. groupId = value1
  823. patchcanvas.joinGroup(groupId)
  824. gCarla.gui.ui.miniCanvasPreview.update()
  825. elif action == patchcanvas.ACTION_PORT_INFO:
  826. pass
  827. elif action == patchcanvas.ACTION_PORT_RENAME:
  828. pass
  829. elif action == patchcanvas.ACTION_PORTS_CONNECT:
  830. gOut, pOut, gIn, pIn = [int(i) for i in valueStr.split(":")]
  831. if not gCarla.host.patchbay_connect(gOut, pOut, gIn, pIn):
  832. print("Connection failed:", gCarla.host.get_last_error())
  833. elif action == patchcanvas.ACTION_PORTS_DISCONNECT:
  834. connectionId = value1
  835. if not gCarla.host.patchbay_disconnect(connectionId):
  836. print("Disconnect failed:", gCarla.host.get_last_error())
  837. elif action == patchcanvas.ACTION_PLUGIN_CLONE:
  838. pluginId = value1
  839. gCarla.host.clone_plugin(pluginId)
  840. elif action == patchcanvas.ACTION_PLUGIN_EDIT:
  841. pluginId = value1
  842. gCarla.gui.fContainer.showEditDialog(pluginId)
  843. elif action == patchcanvas.ACTION_PLUGIN_RENAME:
  844. pluginId = value1
  845. newName = valueStr
  846. gCarla.host.rename_plugin(pluginId, newName)
  847. elif action == patchcanvas.ACTION_PLUGIN_REMOVE:
  848. pluginId = value1
  849. gCarla.host.remove_plugin(pluginId)
  850. elif action == patchcanvas.ACTION_PLUGIN_SHOW_UI:
  851. pluginId = value1
  852. gCarla.host.show_custom_ui(pluginId, True)