Browse Source

Import of jpka changes for rack UI rework

Signed-off-by: falkTX <falktx@falktx.com>
pull/2012/head
falkTX 3 months ago
parent
commit
4bf5db57c2
Signed by: falkTX <falktx@falktx.com> GPG Key ID: CDBAA37ABC74FBA0
63 changed files with 3188 additions and 938 deletions
  1. BIN
      resources/16x16/add-jack-alt.svgz
  2. BIN
      resources/16x16/add-jack.svgz
  3. BIN
      resources/16x16/audio-volume-medium.svgz
  4. BIN
      resources/16x16/audio-volume-muted-orig.svgz
  5. BIN
      resources/16x16/audio-volume-muted.svgz
  6. BIN
      resources/16x16/balance-alt.svgz
  7. BIN
      resources/16x16/balance-alt2.svgz
  8. BIN
      resources/16x16/balance.svgz
  9. BIN
      resources/16x16/compact-alt.svgz
  10. BIN
      resources/16x16/compact.svgz
  11. BIN
      resources/16x16/dry.svgz
  12. BIN
      resources/16x16/emblem-favorite.svgz
  13. BIN
      resources/16x16/restore-alt.svgz
  14. BIN
      resources/16x16/restore.svgz
  15. BIN
      resources/16x16/skin.svgz
  16. BIN
      resources/16x16/system-shutdown.svgz
  17. BIN
      resources/16x16/system-turnon.svgz
  18. BIN
      resources/16x16/view-refresh-purple.svgz
  19. BIN
      resources/16x16/wet.svgz
  20. +14
    -0
      resources/resources.qrc
  21. +101
    -149
      resources/ui/carla_edit.ui
  22. +99
    -1
      resources/ui/carla_host.ui
  23. +21
    -0
      resources/ui/carla_settings.ui
  24. +90
    -14
      resources/ui/xycontroller.ui
  25. +13
    -1
      source/backend/CarlaBackend.h
  26. +7
    -0
      source/backend/CarlaHost.h
  27. +10
    -0
      source/backend/CarlaPlugin.hpp
  28. +8
    -0
      source/backend/CarlaStandalone.cpp
  29. +2
    -0
      source/backend/engine/CarlaEngine.cpp
  30. +11
    -0
      source/backend/engine/CarlaEngineNative.cpp
  31. +1
    -0
      source/backend/engine/CarlaEngineOsc.hpp
  32. +13
    -0
      source/backend/engine/CarlaEngineOscHandlers.cpp
  33. +6
    -5
      source/backend/engine/CarlaEngineOscSend.cpp
  34. +44
    -0
      source/backend/plugin/CarlaPlugin.cpp
  35. +28
    -4
      source/backend/plugin/CarlaPluginFluidSynth.cpp
  36. +1
    -0
      source/backend/plugin/CarlaPluginInternal.cpp
  37. +1
    -0
      source/backend/plugin/CarlaPluginInternal.hpp
  38. +52
    -1
      source/backend/plugin/CarlaPluginLADSPADSSI.cpp
  39. +52
    -1
      source/backend/plugin/CarlaPluginLV2.cpp
  40. +53
    -2
      source/backend/plugin/CarlaPluginNative.cpp
  41. +39
    -2
      source/frontend/carla_backend.py
  42. +6
    -0
      source/frontend/carla_backend_qtweb.py
  43. +147
    -13
      source/frontend/carla_host.py
  44. +4
    -2
      source/frontend/carla_host_control.py
  45. +7
    -0
      source/frontend/carla_settings.py
  46. +57
    -8
      source/frontend/carla_shared.py
  47. +406
    -203
      source/frontend/carla_skin.py
  48. +251
    -82
      source/frontend/carla_widgets.py
  49. +1
    -0
      source/frontend/dialogs/aboutdialog.cpp
  50. +20
    -0
      source/frontend/qt_compat.py
  51. +5
    -3
      source/frontend/widgets/collapsablewidget.py
  52. +408
    -95
      source/frontend/widgets/commondial.py
  53. +37
    -8
      source/frontend/widgets/digitalpeakmeter.py
  54. +16
    -15
      source/frontend/widgets/paramspinbox.py
  55. +10
    -4
      source/frontend/widgets/racklistwidget.py
  56. +840
    -224
      source/frontend/widgets/scalabledial.py
  57. +217
    -98
      source/frontend/xycontroller-ui
  58. +64
    -3
      source/native-plugins/xycontroller.cpp
  59. +14
    -0
      source/rest/carla-host.cpp
  60. +1
    -0
      source/rest/rest-server.cpp
  61. +2
    -0
      source/utils/CarlaBackendUtils.hpp
  62. +8
    -0
      source/utils/CarlaStateUtils.cpp
  63. +1
    -0
      source/utils/CarlaStateUtils.hpp

BIN
resources/16x16/add-jack-alt.svgz View File


BIN
resources/16x16/add-jack.svgz View File


BIN
resources/16x16/audio-volume-medium.svgz View File


BIN
resources/16x16/audio-volume-muted-orig.svgz View File


BIN
resources/16x16/audio-volume-muted.svgz View File


BIN
resources/16x16/balance-alt.svgz View File


BIN
resources/16x16/balance-alt2.svgz View File


BIN
resources/16x16/balance.svgz View File


BIN
resources/16x16/compact-alt.svgz View File


BIN
resources/16x16/compact.svgz View File


BIN
resources/16x16/dry.svgz View File


BIN
resources/16x16/emblem-favorite.svgz View File


BIN
resources/16x16/restore-alt.svgz View File


BIN
resources/16x16/restore.svgz View File


BIN
resources/16x16/skin.svgz View File


BIN
resources/16x16/system-shutdown.svgz View File


BIN
resources/16x16/system-turnon.svgz View File


BIN
resources/16x16/view-refresh-purple.svgz View File


BIN
resources/16x16/wet.svgz View File


+ 14
- 0
resources/resources.qrc View File

@@ -3,9 +3,15 @@
<file>16x16/carla.png</file>
<file>16x16/carla-control.png</file>

<file>16x16/add-from-favorites.svgz</file>
<file>16x16/add-jack.svgz</file>
<file>16x16/application-exit.svgz</file>
<file>16x16/arrow-right.svgz</file>
<file>16x16/audio-volume-medium.svgz</file>
<file>16x16/audio-volume-muted.svgz</file>
<file>16x16/balance.svgz</file>
<file>16x16/bookmarks.svgz</file>
<file>16x16/compact.svgz</file>
<file>16x16/configure.svgz</file>
<file>16x16/dialog-cancel.svgz</file>
<file>16x16/dialog-error.svgz</file>
@@ -16,9 +22,11 @@
<file>16x16/document-open.svgz</file>
<file>16x16/document-save.svgz</file>
<file>16x16/document-save-as.svgz</file>
<file>16x16/dry.svgz</file>
<file>16x16/edit-clear.svgz</file>
<file>16x16/edit-delete.svgz</file>
<file>16x16/edit-rename.svgz</file>
<file>16x16/emblem-favorite.svgz</file>
<file>16x16/list-add.svgz</file>
<file>16x16/list-remove.svgz</file>
<file>16x16/media-playback-pause.svgz</file>
@@ -27,8 +35,14 @@
<file>16x16/media-seek-backward.svgz</file>
<file>16x16/media-seek-forward.svgz</file>
<file>16x16/network-connect.svgz</file>
<file>16x16/restore.svgz</file>
<file>16x16/skin.svgz</file>
<file>16x16/system-shutdown.svgz</file>
<file>16x16/system-turnon.svgz</file>
<file>16x16/view-refresh.svgz</file>
<file>16x16/view-refresh-purple.svgz</file>
<file>16x16/view-sort-ascending.svgz</file>
<file>16x16/wet.svgz</file>
<file>16x16/window-close.svgz</file>
<file>16x16/zoom-fit-best.svgz</file>
<file>16x16/zoom-in.svgz</file>


+ 101
- 149
resources/ui/carla_edit.ui View File

@@ -108,17 +108,17 @@
</spacer>
</item>
<item>
<widget class="ScalableDial" name="dial_drywet">
<widget class="QWidget" name="dial_drywet">
<property name="minimumSize">
<size>
<width>34</width>
<height>34</height>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>34</width>
<height>34</height>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="contextMenuPolicy">
@@ -130,17 +130,17 @@
</widget>
</item>
<item>
<widget class="ScalableDial" name="dial_vol">
<widget class="QWidget" name="dial_vol">
<property name="minimumSize">
<size>
<width>34</width>
<height>34</height>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>34</width>
<height>34</height>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="contextMenuPolicy">
@@ -152,148 +152,92 @@
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<widget class="QWidget" name="dial_b_left">
<property name="minimumSize">
<size>
<width>26</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>42</height>
<width>26</width>
<height>40</height>
</size>
</property>
<property name="lineWidth">
<number>0</number>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="currentIndex">
<number>0</number>
<property name="statusTip">
<string>Balance Left (0%)</string>
</property>
<widget class="QWidget" name="page_bal">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="ScalableDial" name="dial_b_left">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="statusTip">
<string>Balance Left (0%)</string>
</property>
</widget>
</item>
<item>
<widget class="ScalableDial" name="dial_b_right">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="statusTip">
<string>Balance Right (0%)</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_pan">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="ScalableDial" name="dial_pan">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="statusTip">
<string>Balance Right (0%)</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
<widget class="QWidget" name="dial_b_right">
<property name="minimumSize">
<size>
<width>26</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>26</width>
<height>40</height>
</size>
</property>
<item>
<widget class="QRadioButton" name="rb_balance">
<property name="text">
<string>Use Balance</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rb_pan">
<property name="text">
<string>Use Panning</string>
</property>
</widget>
</item>
</layout>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="statusTip">
<string>Balance Right (0%)</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="dial_pan">
<property name="minimumSize">
<size>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="statusTip">
<string>Left-Right (0%)</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="dial_forth">
<property name="minimumSize">
<size>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>48</height>
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="statusTip">
<string>Front-Rear (0%)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
@@ -310,6 +254,21 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_ctrl_channel_2">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>&#9888; L, R are special mixing type.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -876,13 +835,6 @@ Plugin Name
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ScalableDial</class>
<extends>QDial</extends>
<header>widgets/scalabledial.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources.qrc"/>
</resources>


+ 99
- 1
resources/ui/carla_host.ui View File

@@ -474,6 +474,7 @@
<addaction name="act_file_refresh"/>
<addaction name="act_file_new"/>
<addaction name="act_file_open"/>
<addaction name="act_file_reload"/>
<addaction name="act_file_save"/>
<addaction name="act_file_save_as"/>
<addaction name="separator"/>
@@ -508,6 +509,7 @@
<addaction name="separator"/>
<addaction name="act_plugins_center"/>
<addaction name="separator"/>
<addaction name="act_plugins_change_skin"/>
<addaction name="act_plugins_compact"/>
<addaction name="act_plugins_expand"/>
</widget>
@@ -548,6 +550,7 @@
<string>&amp;Settings</string>
</property>
<addaction name="act_settings_show_toolbar"/>
<addaction name="act_settings_show_toolbar_text"/>
<addaction name="act_settings_show_meters"/>
<addaction name="act_settings_show_keyboard"/>
<addaction name="act_settings_show_side_panel"/>
@@ -589,6 +592,7 @@
</attribute>
<addaction name="act_file_new"/>
<addaction name="act_file_open"/>
<addaction name="act_file_reload"/>
<addaction name="act_file_save"/>
<addaction name="act_file_save_as"/>
<addaction name="act_file_connect"/>
@@ -600,6 +604,17 @@
<addaction name="act_engine_panic"/>
<addaction name="separator"/>
<addaction name="act_settings_configure"/>
<addaction name="separator"/>
<addaction name="act_plugins_enable"/>
<addaction name="act_plugins_disable"/>
<addaction name="act_plugins_bypass"/>
<addaction name="act_plugins_wet100"/>
<addaction name="act_plugins_mute"/>
<addaction name="act_plugins_volume100"/>
<addaction name="act_plugins_center"/>
<addaction name="act_plugins_change_skin"/>
<addaction name="act_plugins_compact"/>
<addaction name="act_plugins_expand"/>
</widget>
<widget class="QDockWidget" name="dockWidget">
<property name="sizePolicy">
@@ -1111,6 +1126,30 @@
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="act_file_reload">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/view-refresh-purple.svgz</normaloff>:/16x16/view-refresh-purple.svgz</iconset>
</property>
<property name="text">
<string>&amp;Reload (!)</string>
</property>
<property name="iconText">
<string>Reload (!)</string>
</property>
<property name="toolTip">
<string>Reload file. CAUTION, non-saved changes will be LOST!</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+R</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="act_file_save">
<property name="icon">
<iconset resource="../resources.qrc">
@@ -1220,6 +1259,10 @@
</property>
</action>
<action name="act_plugins_enable">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/system-turnon.svgz</normaloff>:/16x16/system-turnon.svgz</iconset>
</property>
<property name="text">
<string>Enable</string>
</property>
@@ -1228,6 +1271,10 @@
</property>
</action>
<action name="act_plugins_disable">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/system-shutdown.svgz</normaloff>:/16x16/system-shutdown.svgz</iconset>
</property>
<property name="text">
<string>Disable</string>
</property>
@@ -1236,6 +1283,10 @@
</property>
</action>
<action name="act_plugins_bypass">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/dry.svgz</normaloff>:/16x16/dry.svgz</iconset>
</property>
<property name="text">
<string>0% Wet (Bypass)</string>
</property>
@@ -1244,6 +1295,10 @@
</property>
</action>
<action name="act_plugins_wet100">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/wet.svgz</normaloff>:/16x16/wet.svgz</iconset>
</property>
<property name="text">
<string>100% Wet</string>
</property>
@@ -1252,6 +1307,10 @@
</property>
</action>
<action name="act_plugins_mute">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/audio-volume-muted.svgz</normaloff>:/16x16/audio-volume-muted.svgz</iconset>
</property>
<property name="text">
<string>0% Volume (Mute)</string>
</property>
@@ -1260,6 +1319,10 @@
</property>
</action>
<action name="act_plugins_volume100">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/audio-volume-medium.svgz</normaloff>:/16x16/audio-volume-medium.svgz</iconset>
</property>
<property name="text">
<string>100% Volume</string>
</property>
@@ -1268,6 +1331,10 @@
</property>
</action>
<action name="act_plugins_center">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/balance.svgz</normaloff>:/16x16/balance.svgz</iconset>
</property>
<property name="text">
<string>Center Balance</string>
</property>
@@ -1435,6 +1502,17 @@
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="act_settings_show_toolbar_text">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Show Toolbar Text</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="act_settings_configure">
<property name="icon">
<iconset resource="../resources.qrc">
@@ -1553,7 +1631,23 @@
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="act_plugins_change_skin">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/skin.svgz</normaloff>:/16x16/skin.svgz</iconset>
</property>
<property name="text">
<string>Change &amp;Skin...</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="act_plugins_compact">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/compact.svgz</normaloff>:/16x16/compact.svgz</iconset>
</property>
<property name="text">
<string>Compact Slots</string>
</property>
@@ -1562,6 +1656,10 @@
</property>
</action>
<action name="act_plugins_expand">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/restore.svgz</normaloff>:/16x16/restore.svgz</iconset>
</property>
<property name="text">
<string>Expand Slots</string>
</property>
@@ -1597,7 +1695,7 @@
<action name="act_plugin_add_jack">
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/16x16/list-add.svgz</normaloff>:/16x16/list-add.svgz</iconset>
<normaloff>:/16x16/add-jack.svgz</normaloff>:/16x16/add-jack.svgz</iconset>
</property>
<property name="text">
<string>Add &amp;JACK Application...</string>


+ 21
- 0
resources/ui/carla_settings.ui View File

@@ -511,6 +511,27 @@
</property>
</spacer>
</item>
<item row="2" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_28">
<item>
<widget class="QLabel" name="label_main_skin_tweaks_1">
<property name="toolTip">
<string>Example: 'Tweak' is for all skins; extra 'skinnameTweak' overrides it for that skin only.</string>
</property>
<property name="text">
<string>Skin tweaks:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="le_main_skin_tweaks">
<property name="toolTip">
<string>Example: 'Tweak' is for all skins; extra 'skinnameTweak' overrides it for that skin only.</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>


+ 90
- 14
resources/ui/xycontroller.ui View File

@@ -30,41 +30,117 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="ScalableDial" name="dial_x">
<property name="minimum">
<number>-100</number>
<spacer name="verticalSpacer0">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="minimumSize">
<size>
<width>48</width>
<height>0</height>
</size>
</property>
<property name="maximum">
<number>100</number>
</spacer>
</item>
<item>
<widget class="QWidget" name="dial_x">
<property name="minimumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<widget class="QWidget" name="dial_out_x">
<property name="minimumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer1">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<property name="minimumSize">
<size>
<width>20</width>
<height>30</height>
<width>48</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="ScalableDial" name="dial_y">
<property name="minimum">
<number>-100</number>
<widget class="QWidget" name="dial_y">
<property name="minimumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
<property name="maximum">
<number>100</number>
</widget>
</item>
<item>
<widget class="QWidget" name="dial_out_y">
<property name="minimumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>58</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="minimumSize">
<size>
<width>48</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>


+ 13
- 1
source/backend/CarlaBackend.h View File

@@ -210,6 +210,11 @@ static constexpr const uint PLUGIN_HAS_CUSTOM_UI_USING_FILE_OPEN = 0x2000;
*/
static constexpr const uint PLUGIN_NEEDS_MAIN_THREAD_IDLE = 0x4000;

/*!
* Plugin can use internal Front-Rear balance for Quadro (or > 2 channels in General).
*/
static constexpr const uint PLUGIN_CAN_FORTH = 0x8000;

/** @} */

/* ------------------------------------------------------------------------------------------------------------
@@ -833,12 +838,19 @@ typedef enum {
* Range -1...15 (-1 = off).
*/
PARAMETER_CTRL_CHANNEL = -8,

/*!
* Experimental Front-Rear parameter for Quadro (or > 2 channels in General).
* Range -1.0...1.0; default is 0.0.
*/
PARAMETER_FORTH = -9,

#endif

/*!
* Max value, defined only for convenience.
*/
PARAMETER_MAX = -9
PARAMETER_MAX = -10

} InternalParameterIndex;



+ 7
- 0
source/backend/CarlaHost.h View File

@@ -966,6 +966,13 @@ CARLA_API_EXPORT void carla_set_balance_right(CarlaHostHandle handle, uint plugi
*/
CARLA_API_EXPORT void carla_set_panning(CarlaHostHandle handle, uint pluginId, float value);

/*!
* Change a plugin's internal experimental front-rear (forth) value.
* @param pluginId Plugin
* @param value New value
*/
CARLA_API_EXPORT void carla_set_forth(CarlaHostHandle handle, uint pluginId, float value);

/*!
* Change a plugin's internal control channel.
* @param pluginId Plugin


+ 10
- 0
source/backend/CarlaPlugin.hpp View File

@@ -542,6 +542,15 @@ public:
*/
void setPanning(float value, bool sendOsc, bool sendCallback) noexcept;

/*!
* Set the plugin's output forth (front-rear) value to @a value.
* @a value must be between -1.0 and 1.0.
*
* @param sendOsc Send message change over OSC
* @param sendCallback Send message change to registered callback
*/
void setForth(float value, bool sendOsc, bool sendCallback) noexcept;

/*!
* Overloaded functions, to be called from within RT context only.
*/
@@ -550,6 +559,7 @@ public:
void setBalanceLeftRT(float value, bool sendCallbackLater) noexcept;
void setBalanceRightRT(float value, bool sendCallbackLater) noexcept;
void setPanningRT(float value, bool sendCallbackLater) noexcept;
void setForthRT(float value, bool sendCallbackLater) noexcept;
#endif // ! BUILD_BRIDGE_ALTERNATIVE_ARCH

/*!


+ 8
- 0
source/backend/CarlaStandalone.cpp View File

@@ -2091,6 +2091,14 @@ void carla_set_panning(CarlaHostHandle handle, uint pluginId, float value)
plugin->setPanning(value, true, false);
}

void carla_set_forth(CarlaHostHandle handle, uint pluginId, float value)
{
CARLA_SAFE_ASSERT_RETURN(handle->engine != nullptr,);

if (const CarlaPluginPtr plugin = handle->engine->getPlugin(pluginId))
plugin->setForth(value, true, false);
}

void carla_set_ctrl_channel(CarlaHostHandle handle, uint pluginId, int8_t channel)
{
CARLA_SAFE_ASSERT_RETURN(handle->engine != nullptr,);


+ 2
- 0
source/backend/engine/CarlaEngine.cpp View File

@@ -2921,6 +2921,7 @@ bool CarlaEngine::loadProjectInternal(water::XmlDocument& xmlDoc, const bool alw
plugin->setBalanceLeft(stateSave.balanceLeft, true, true);
plugin->setBalanceRight(stateSave.balanceRight, true, true);
plugin->setPanning(stateSave.panning, true, true);
plugin->setForth(stateSave.forth, true, true);
plugin->setCtrlChannel(stateSave.ctrlChannel, true, true);
plugin->setActive(stateSave.active, true, true);
plugin->setEnabled(true);
@@ -2976,6 +2977,7 @@ bool CarlaEngine::loadProjectInternal(water::XmlDocument& xmlDoc, const bool alw
plugin->setBalanceLeft(stateSave.balanceLeft, true, true);
plugin->setBalanceRight(stateSave.balanceRight, true, true);
plugin->setPanning(stateSave.panning, true, true);
plugin->setForth(stateSave.forth, true, true);
plugin->setCtrlChannel(stateSave.ctrlChannel, true, true);
plugin->setActive(stateSave.active, true, true);
plugin->setEnabled(true);


+ 11
- 0
source/backend/engine/CarlaEngineNative.cpp View File

@@ -2115,6 +2115,17 @@ bool CarlaEngineNativeUI::msgReceived(const char* const msg) noexcept
if (const CarlaPluginPtr plugin = fEngine->getPlugin(pluginId))
plugin->setPanning(value, true, false);
}
else if (std::strcmp(msg, "set_forth") == 0)
{
uint32_t pluginId;
float value;

CARLA_SAFE_ASSERT_RETURN(readNextLineAsUInt(pluginId), true);
CARLA_SAFE_ASSERT_RETURN(readNextLineAsFloat(value), true);

if (const CarlaPluginPtr plugin = fEngine->getPlugin(pluginId))
plugin->setForth(value, true, false);
}
else if (std::strcmp(msg, "set_ctrl_channel") == 0)
{
uint32_t pluginId;


+ 1
- 0
source/backend/engine/CarlaEngineOsc.hpp View File

@@ -137,6 +137,7 @@ private:
int handleMsgSetBalanceLeft(CARLA_ENGINE_OSC_HANDLE_ARGS);
int handleMsgSetBalanceRight(CARLA_ENGINE_OSC_HANDLE_ARGS);
int handleMsgSetPanning(CARLA_ENGINE_OSC_HANDLE_ARGS);
int handleMsgSetForth(CARLA_ENGINE_OSC_HANDLE_ARGS);
int handleMsgSetParameterValue(CARLA_ENGINE_OSC_HANDLE_ARGS);
int handleMsgSetParameterMappedControlIndex(CARLA_ENGINE_OSC_HANDLE_ARGS);
int handleMsgSetParameterMappedRange(CARLA_ENGINE_OSC_HANDLE_ARGS);


+ 13
- 0
source/backend/engine/CarlaEngineOscHandlers.cpp View File

@@ -179,6 +179,8 @@ int CarlaEngineOsc::handleMessage(const bool isTCP, const char* const path,
return handleMsgSetBalanceRight(plugin, argc, argv, types);
if (std::strcmp(method, "set_panning") == 0)
return handleMsgSetPanning(plugin, argc, argv, types);
if (std::strcmp(method, "set_forth") == 0)
return handleMsgSetForth(plugin, argc, argv, types);
if (std::strcmp(method, "set_ctrl_channel") == 0)
return 0; //handleMsgSetControlChannel(plugin, argc, argv, types); // TODO
if (std::strcmp(method, "set_parameter_value") == 0)
@@ -678,6 +680,17 @@ int CarlaEngineOsc::handleMsgSetPanning(CARLA_ENGINE_OSC_HANDLE_ARGS)
return 0;
}

int CarlaEngineOsc::handleMsgSetForth(CARLA_ENGINE_OSC_HANDLE_ARGS)
{
carla_debug("CarlaEngineOsc::handleMsgSetForth()");
CARLA_ENGINE_OSC_CHECK_OSC_TYPES(1, "f");

const float value = argv[0]->f;

plugin->setForth(value, false, true);
return 0;
}

int CarlaEngineOsc::handleMsgSetParameterValue(CARLA_ENGINE_OSC_HANDLE_ARGS)
{
carla_debug("CarlaEngineOsc::handleMsgSetParameterValue()");


+ 6
- 5
source/backend/engine/CarlaEngineOscSend.cpp View File

@@ -287,18 +287,18 @@ void CarlaEngineOsc::sendPluginInternalParameterValues(const CarlaPluginPtr& plu
carla_debug("CarlaEngineOsc::sendPluginInternalParameterValues(%p)", plugin.get());

#ifdef CARLA_PROPER_CPP11_SUPPORT
static_assert(PARAMETER_ACTIVE == -2 && PARAMETER_MAX == -9, "Incorrect data");
static_assert(PARAMETER_ACTIVE == -2 && PARAMETER_MAX == -10, "Incorrect data");
#endif

double iparams[7];
double iparams[8];

for (int32_t i = 0; i < 7; ++i)
for (int32_t i = 0; i < 8; ++i)
iparams[i] = plugin->getInternalParameterValue(PARAMETER_ACTIVE - i);

char targetPath[std::strlen(fControlDataTCP.path)+9];
std::strcpy(targetPath, fControlDataTCP.path);
std::strcat(targetPath, "/iparams");
try_lo_send(fControlDataTCP.target, targetPath, "ifffffff",
try_lo_send(fControlDataTCP.target, targetPath, "iffffffff",
static_cast<int32_t>(plugin->getId()),
iparams[0], // PARAMETER_ACTIVE
iparams[1], // PARAMETER_DRYWET
@@ -306,7 +306,8 @@ void CarlaEngineOsc::sendPluginInternalParameterValues(const CarlaPluginPtr& plu
iparams[3], // PARAMETER_BALANCE_LEFT
iparams[4], // PARAMETER_BALANCE_RIGHT
iparams[5], // PARAMETER_PANNING
iparams[6] // PARAMETER_CTRL_CHANNEL
iparams[6], // PARAMETER_CTRL_CHANNEL
iparams[7] // PARAMETER_FORTH
);
}



+ 44
- 0
source/backend/plugin/CarlaPlugin.cpp View File

@@ -392,6 +392,8 @@ float CarlaPlugin::getInternalParameterValue(const int32_t parameterId) const no
return pData->postProc.balanceRight;
case PARAMETER_PANNING:
return pData->postProc.panning;
case PARAMETER_FORTH:
return pData->postProc.forth;
};
#endif
CARLA_SAFE_ASSERT_RETURN(parameterId >= 0, 0.0f);
@@ -540,6 +542,7 @@ const CarlaStateSave& CarlaPlugin::getStateSave(const bool callPrepareForSave)
pData->stateSave.balanceLeft = pData->postProc.balanceLeft;
pData->stateSave.balanceRight = pData->postProc.balanceRight;
pData->stateSave.panning = pData->postProc.panning;
pData->stateSave.forth = pData->postProc.forth;
pData->stateSave.ctrlChannel = pData->ctrlChannel;
#endif

@@ -953,6 +956,7 @@ void CarlaPlugin::loadStateSave(const CarlaStateSave& stateSave)
setBalanceLeft(stateSave.balanceLeft, true, true);
setBalanceRight(stateSave.balanceRight, true, true);
setPanning(stateSave.panning, true, true);
setForth(stateSave.forth, true, true);
setCtrlChannel(stateSave.ctrlChannel, true, true);
setActive(stateSave.active, true, true);

@@ -1550,6 +1554,31 @@ void CarlaPlugin::setPanning(const float value, const bool sendOsc, const bool s
nullptr);
}

void CarlaPlugin::setForth(const float value, const bool sendOsc, const bool sendCallback) noexcept
{
if (pData->engineBridged) {
CARLA_SAFE_ASSERT_RETURN(!sendOsc && !sendCallback,);
} else {
CARLA_SAFE_ASSERT_RETURN(sendOsc || sendCallback,); // never call this from RT
}
CARLA_SAFE_ASSERT(value >= -1.0f && value <= 1.0f);

const float fixedValue(carla_fixedValue<float>(-1.0f, 1.0f, value));

if (carla_isEqual(pData->postProc.forth, fixedValue))
return;

pData->postProc.forth = fixedValue;

pData->engine->callback(sendCallback, sendOsc,
ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED,
pData->id,
PARAMETER_FORTH,
0, 0,
fixedValue,
nullptr);
}

void CarlaPlugin::setDryWetRT(const float value, const bool sendCallbackLater) noexcept
{
CARLA_SAFE_ASSERT(value >= 0.0f && value <= 1.0f);
@@ -1614,6 +1643,19 @@ void CarlaPlugin::setPanningRT(const float value, const bool sendCallbackLater)
pData->postProc.panning = fixedValue;
pData->postponeParameterChangeRtEvent(sendCallbackLater, PARAMETER_PANNING, fixedValue);
}

void CarlaPlugin::setForthRT(const float value, const bool sendCallbackLater) noexcept
{
CARLA_SAFE_ASSERT(value >= -1.0f && value <= 1.0f);

const float fixedValue(carla_fixedValue<float>(-1.0f, 1.0f, value));

if (carla_isEqual(pData->postProc.forth, fixedValue))
return;

pData->postProc.forth = fixedValue;
pData->postponeParameterChangeRtEvent(sendCallbackLater, PARAMETER_FORTH, fixedValue);
}
#endif // ! BUILD_BRIDGE_ALTERNATIVE_ARCH

void CarlaPlugin::setCtrlChannel(const int8_t channel, const bool sendOsc, const bool sendCallback) noexcept
@@ -1695,6 +1737,8 @@ void CarlaPlugin::setParameterValueByRealIndex(const int32_t rindex, const float
return setBalanceRight(value, sendOsc, sendCallback);
case PARAMETER_PANNING:
return setPanning(value, sendOsc, sendCallback);
case PARAMETER_FORTH:
return setForth(value, sendOsc, sendCallback);
}
#endif
CARLA_SAFE_ASSERT_RETURN(rindex >= 0,);


+ 28
- 4
source/backend/plugin/CarlaPluginFluidSynth.cpp View File

@@ -947,7 +947,7 @@ public:
pData->hints |= PLUGIN_USES_MULTI_PROGS;

if (! kUse16Outs)
pData->hints |= PLUGIN_CAN_BALANCE;
pData->hints |= PLUGIN_CAN_BALANCE | PLUGIN_CAN_PANNING;

// extra plugin hints
pData->extraHints = 0x0;
@@ -1521,7 +1521,7 @@ public:

{
// note - balance not possible with kUse16Outs, so we can safely skip fAudioOutBuffers
const bool doVolume = (pData->hints & PLUGIN_CAN_VOLUME) != 0 && carla_isNotEqual(pData->postProc.volume, 1.0f);
const bool doVolume = (pData->hints & PLUGIN_CAN_VOLUME) != 0 && (carla_isNotEqual(pData->postProc.volume, 1.0f) || carla_isNotEqual(pData->postProc.panning, 0.0f));
const bool doBalance = (pData->hints & PLUGIN_CAN_BALANCE) != 0 && ! (carla_isEqual(pData->postProc.balanceLeft, -1.0f) && carla_isEqual(pData->postProc.balanceRight, 1.0f));

float* const oldBufLeft = pData->postProc.extraBuffer;
@@ -1554,16 +1554,40 @@ public:
}
}

// Panning
// Only decrease of levels, but never increase, unlike 'L, R'.
// Note: no any pan processing for Mono.

uint32_t q = pData->audioOut.count;
float pan = pData->postProc.panning;
float vol = pData->postProc.volume;

// Pan: Stereo only.
if ((pan != 0.0) and (q == 2))
{
// left channel(s) reduce when pan to right
if ((pan > 0) && (i == 0))
{
vol = vol * (1.0 - pan);
}

// right channel(s) reduce when pan to left
else if ((pan < 0) && (i == 1))
{
vol = vol * (1.0 + pan);
}
}

// Volume
if (kUse16Outs)
{
for (uint32_t k=0; k < frames; ++k)
outBuffer[i][k+timeOffset] = fAudio16Buffers[i][k] * pData->postProc.volume;
outBuffer[i][k+timeOffset] = fAudio16Buffers[i][k] * vol;
}
else if (doVolume)
{
for (uint32_t k=0; k < frames; ++k)
outBuffer[i][k+timeOffset] *= pData->postProc.volume;
outBuffer[i][k+timeOffset] *= vol;
}
}



+ 1
- 0
source/backend/plugin/CarlaPluginInternal.cpp View File

@@ -710,6 +710,7 @@ CarlaPlugin::ProtectedData::PostProc::PostProc() noexcept
balanceLeft(-1.0f),
balanceRight(1.0f),
panning(0.0f),
forth(0.0f),
extraBuffer(nullptr) {}
#endif



+ 1
- 0
source/backend/plugin/CarlaPluginInternal.hpp View File

@@ -379,6 +379,7 @@ struct CarlaPlugin::ProtectedData {
float balanceLeft;
float balanceRight;
float panning;
float forth;
float* extraBuffer;

PostProc() noexcept;


+ 52
- 1
source/backend/plugin/CarlaPluginLADSPADSSI.cpp View File

@@ -1270,6 +1270,12 @@ public:

if (aOuts >= 2 && aOuts % 2 == 0)
pData->hints |= PLUGIN_CAN_BALANCE;

if (aOuts >= 2)
pData->hints |= PLUGIN_CAN_PANNING;

if (aOuts >= 3)
pData->hints |= PLUGIN_CAN_FORTH;
#endif

// extra plugin hints
@@ -2082,7 +2088,11 @@ public:
fAudioOutBuffers[i][k] = (fAudioOutBuffers[i][k] * pData->postProc.dryWet) + (bufValue * (1.0f - pData->postProc.dryWet));
}
}
}

// Do not join this loop with loop above.
for (uint32_t i=0; i < pData->audioOut.count; ++i)
{
// Balance
if (doBalance)
{
@@ -2114,10 +2124,51 @@ public:
}
}

// Panning and Front-Rear ("Forth").
// Only decrease of levels, but never increase, unlike 'L, R'.
// Note: no any pan/forth processing for Mono.

uint32_t q = pData->audioOut.count;
float pan = pData->postProc.panning;
float forth = pData->postProc.forth;
float vol = pData->postProc.volume;

// Pan: Stereo, 3 ch (extra rear/bass), or Quadro.
if ((pan != 0.0) and ((q == 2) || (q == 3) || (q == 4)))
{
// left channel(s) reduce when pan to right
if ((pan > 0) && ((i == 0) || ((i == 2) && (q == 4))))
{
vol = vol * (1.0 - pan);
}

// right channel(s) reduce when pan to left
else if ((pan < 0) && ((i == 1) || (i == 3)))
{
vol = vol * (1.0 + pan);
}
}

// Front-Rear: 3 ch (extra rear/bass), or Quadro.
if ((forth != 0.0) and ((q == 3) || (q == 4)))
{
// rear channel(s) reduce when moving forth to front
if ((forth > 0) && ((i == 2) || (i == 3)))
{
vol = vol * (1.0 - forth);
}

// front channels reduce when moving back to rear
else if ((forth < 0) && ((i == 0) || (i == 1)))
{
vol = vol * (1.0 + forth);
}
}

// Volume (and buffer copy)
{
for (uint32_t k=0; k < frames; ++k)
audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * pData->postProc.volume;
audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * vol;
}
}



+ 52
- 1
source/backend/plugin/CarlaPluginLV2.cpp View File

@@ -3338,6 +3338,12 @@ public:
if (aOuts >= 2 && aOuts % 2 == 0)
pData->hints |= PLUGIN_CAN_BALANCE;

if (aOuts >= 2)
pData->hints |= PLUGIN_CAN_PANNING;

if (aOuts >= 3)
pData->hints |= PLUGIN_CAN_FORTH;

// extra plugin hints
pData->extraHints = 0x0;

@@ -4673,7 +4679,11 @@ public:
fAudioOutBuffers[i][k] = (fAudioOutBuffers[i][k] * pData->postProc.dryWet) + (bufValue * (1.0f - pData->postProc.dryWet));
}
}
}

// Do not join this loop with loop above.
for (uint32_t i=0; i < pData->audioOut.count; ++i)
{
// Balance
if (doBalance)
{
@@ -4705,10 +4715,51 @@ public:
}
}

// Panning and Front-Rear ("Forth").
// Only decrease of levels, but never increase, unlike 'L, R'.
// Note: no any pan/forth processing for Mono.

uint32_t q = pData->audioOut.count;
float pan = pData->postProc.panning;
float forth = pData->postProc.forth;
float vol = pData->postProc.volume;

// Pan: Stereo, 3 ch (extra rear/bass), or Quadro.
if ((pan != 0.0) and ((q == 2) || (q == 3) || (q == 4)))
{
// left channel(s) reduce when pan to right
if ((pan > 0) && ((i == 0) || ((i == 2) && (q == 4))))
{
vol = vol * (1.0 - pan);
}

// right channel(s) reduce when pan to left
else if ((pan < 0) && ((i == 1) || (i == 3)))
{
vol = vol * (1.0 + pan);
}
}

// Front-Rear: 3 ch (extra rear/bass), or Quadro.
if ((forth != 0.0) and ((q == 3) || (q == 4)))
{
// rear channel(s) reduce when moving forth to front
if ((forth > 0) && ((i == 2) || (i == 3)))
{
vol = vol * (1.0 - forth);
}

// front channels reduce when moving back to rear
else if ((forth < 0) && ((i == 0) || (i == 1)))
{
vol = vol * (1.0 + forth);
}
}

// Volume (and buffer copy)
{
for (uint32_t k=0; k < frames; ++k)
audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * pData->postProc.volume;
audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * vol;
}
}
} // End of Post-processing


+ 53
- 2
source/backend/plugin/CarlaPluginNative.cpp View File

@@ -1405,6 +1405,12 @@ public:
if (aOuts >= 2 && aOuts % 2 == 0)
pData->hints |= PLUGIN_CAN_BALANCE;

if (aOuts >= 2)
pData->hints |= PLUGIN_CAN_PANNING;

if (aOuts >= 3)
pData->hints |= PLUGIN_CAN_FORTH;

// native plugin hints
if (fDescriptor->hints & NATIVE_PLUGIN_IS_RTSAFE)
pData->hints |= PLUGIN_IS_RTSAFE;
@@ -2369,7 +2375,7 @@ public:
float bufValue;
float* const oldBufLeft = pData->postProc.extraBuffer;

for (; i < pData->audioOut.count; ++i)
for (uint32_t i=0; i < pData->audioOut.count; ++i)
{
// Dry/Wet
if (doDryWet)
@@ -2380,7 +2386,11 @@ public:
fAudioAndCvOutBuffers[i][k] = (fAudioAndCvOutBuffers[i][k] * pData->postProc.dryWet) + (bufValue * (1.0f - pData->postProc.dryWet));
}
}
}

// Do not join this loop with loop above.
for (uint32_t i=0; i < pData->audioOut.count; ++i)
{
// Balance
if (doBalance)
{
@@ -2412,10 +2422,51 @@ public:
}
}

// Panning and Front-Rear ("Forth").
// Only decrease of levels, but never increase, unlike 'L, R'.
// Note: no any pan/forth processing for Mono.

uint32_t q = pData->audioOut.count;
float pan = pData->postProc.panning;
float forth = pData->postProc.forth;
float vol = pData->postProc.volume;

// Pan: Stereo, 3 ch (extra rear/bass), or Quadro.
if ((pan != 0.0) and ((q == 2) || (q == 3) || (q == 4)))
{
// left channel(s) reduce when pan to right
if ((pan > 0) && ((i == 0) || ((i == 2) && (q == 4))))
{
vol = vol * (1.0 - pan);
}

// right channel(s) reduce when pan to left
else if ((pan < 0) && ((i == 1) || (i == 3)))
{
vol = vol * (1.0 + pan);
}
}

// Front-Rear: 3 ch (extra rear/bass), or Quadro.
if ((forth != 0.0) and ((q == 3) || (q == 4)))
{
// rear channel(s) reduce when moving forth to front
if ((forth > 0) && ((i == 2) || (i == 3)))
{
vol = vol * (1.0 - forth);
}

// front channels reduce when moving back to rear
else if ((forth < 0) && ((i == 0) || (i == 1)))
{
vol = vol * (1.0 + forth);
}
}

// Volume (and buffer copy)
{
for (uint32_t k=0; k < frames; ++k)
audioOut[i][k+timeOffset] = fAudioAndCvOutBuffers[i][k] * pData->postProc.volume;
audioOut[i][k+timeOffset] = fAudioAndCvOutBuffers[i][k] * vol;
}
}



+ 39
- 2
source/frontend/carla_backend.py View File

@@ -207,6 +207,9 @@ PLUGIN_USES_MULTI_PROGS = 0x400
# Plugin can make use of inline display API.
PLUGIN_HAS_INLINE_DISPLAY = 0x800

# Plugin can use internal control of Front-Rear balance for Quadro (or > 2 channels in General).
PLUGIN_CAN_FORTH = 0x8000

# ---------------------------------------------------------------------------------------------------------------------
# Plugin Options
# Various plugin options.
@@ -295,6 +298,16 @@ PARAMETER_CAN_BE_CV_CONTROLLED = 0x800
# @note only valid for parameter inputs.
PARAMETER_IS_NOT_SAVED = 0x1000

# Human readable labels for 24 decoded bits (currently for XRay tab of Edit dialog).
# Are some hints can exceed 2^24 ?
parameterHintsText = (
"IS_BOOLEAN", "IS_INTEGER", "IS_LOGARITHMIC", "n/a",
"IS_ENABLED", "IS_AUTOMATABLE", "IS_READ_ONLY", "n/a",
"USES_SAMPLERATE", "USES_SCALEPOINTS", "USES_CUSTOM_TEXT", "CAN_BE_CV_CONTROLLED",
"IS_NOT_SAVED", "n/a", "n/a", "n/a",
"n/a", "n/a", "n/a", "n/a",
"n/a", "n/a", "n/a", "n/a", )

# ---------------------------------------------------------------------------------------------------------------------
# Mapped Parameter Flags
# Various flags for parameter mappings.
@@ -543,8 +556,12 @@ PARAMETER_PANNING = -7
# Range -1...15 (-1 = off).
PARAMETER_CTRL_CHANNEL = -8

# Experimental Front-Rear (Forth) parameter.
# Range -1.0...1.0; default is 0.0.
PARAMETER_FORTH = -9

# Max value, defined only for convenience.
PARAMETER_MAX = -9
PARAMETER_MAX = -10

# ---------------------------------------------------------------------------------------------------------------------
# Special Mapped Control Index
@@ -2102,6 +2119,13 @@ class CarlaHostMeta():
def set_panning(self, pluginId, value):
raise NotImplementedError

# Change a plugin's internal front-rear (forth) value.
# @param pluginId Plugin
# @param value New value
@abstractmethod
def set_forth(self, pluginId, value):
raise NotImplementedError

# Change a plugin's internal control channel.
# @param pluginId Plugin
# @param channel New channel
@@ -2520,6 +2544,9 @@ class CarlaHostNull(CarlaHostMeta):
def set_panning(self, pluginId, value):
return

def set_forth(self, pluginId, value):
return

def set_ctrl_channel(self, pluginId, channel):
return

@@ -2842,6 +2869,9 @@ class CarlaHostDLL(CarlaHostMeta):
self.lib.carla_set_panning.argtypes = (c_void_p, c_uint, c_float)
self.lib.carla_set_panning.restype = None

self.lib.carla_set_forth.argtypes = (c_void_p, c_uint, c_float)
self.lib.carla_set_forth.restype = None

self.lib.carla_set_ctrl_channel.argtypes = (c_void_p, c_uint, c_int8)
self.lib.carla_set_ctrl_channel.restype = None

@@ -3183,6 +3213,9 @@ class CarlaHostDLL(CarlaHostMeta):
def set_panning(self, pluginId, value):
self.lib.carla_set_panning(self.handle, pluginId, value)

def set_forth(self, pluginId, value):
self.lib.carla_set_forth(self.handle, pluginId, value)

def set_ctrl_channel(self, pluginId, channel):
self.lib.carla_set_ctrl_channel(self.handle, pluginId, channel)

@@ -3263,7 +3296,7 @@ class PluginStoreInfo():
def clear(self):
self.pluginInfo = PyCarlaPluginInfo.copy()
self.pluginRealName = ""
self.internalValues = [0.0, 1.0, 1.0, -1.0, 1.0, 0.0, -1.0]
self.internalValues = [0.0, 1.0, 1.0, -1.0, 1.0, 0.0, -1.0, 0.0]
self.audioCountInfo = PyCarlaPortCountInfo.copy()
self.midiCountInfo = PyCarlaPortCountInfo.copy()
self.parameterCount = 0
@@ -3590,6 +3623,10 @@ class CarlaHostPlugin(CarlaHostMeta):
self.sendMsg(["set_panning", pluginId, value])
self.fPluginsInfo[pluginId].internalValues[5] = value

def set_forth(self, pluginId, value):
self.sendMsg(["set_forth", pluginId, value])
self.fPluginsInfo[pluginId].internalValues[7] = value

def set_ctrl_channel(self, pluginId, channel):
self.sendMsg(["set_ctrl_channel", pluginId, channel])
self.fPluginsInfo[pluginId].internalValues[6] = float(channel)


+ 6
- 0
source/frontend/carla_backend_qtweb.py View File

@@ -468,6 +468,12 @@ class CarlaHostQtWeb(CarlaHostQtNull):
'value': value,
})

def set_forth(self, pluginId, value):
requests.get("{}/set_forth".format(self.baseurl), params={
'pluginId': pluginId,
'value': value,
})

def set_ctrl_channel(self, pluginId, channel):
requests.get("{}/set_ctrl_channel".format(self.baseurl), params={
'pluginId': pluginId,


+ 147
- 13
source/frontend/carla_host.py View File

@@ -54,6 +54,7 @@ if qt_config == 5:
QListWidgetItem,
QGraphicsView,
QMainWindow,
QToolButton,
)

elif qt_config == 6:
@@ -86,6 +87,7 @@ elif qt_config == 6:
QListWidgetItem,
QGraphicsView,
QMainWindow,
QToolButton,
)

# ------------------------------------------------------------------------------------------------------------
@@ -101,6 +103,7 @@ from carla_shared import *
from carla_settings import *
from carla_utils import *
from carla_widgets import *
from carla_skin import *

from patchcanvas import patchcanvas
from widgets.digitalpeakmeter import DigitalPeakMeter
@@ -221,6 +224,8 @@ class HostWindow(QMainWindow):
self.fOscAddressTCP = ""
self.fOscAddressUDP = ""

self.slowTimer = 0

if CARLA_OS_MAC:
self.fMacClosingHelper = True

@@ -270,6 +275,8 @@ class HostWindow(QMainWindow):

self.fWithCanvas = withCanvas

self.fTweaks = {}

# ----------------------------------------------------------------------------------------------------
# Internal stuff (logs)

@@ -293,6 +300,7 @@ class HostWindow(QMainWindow):
if self.host.isControl:
self.ui.act_file_new.setVisible(False)
self.ui.act_file_open.setVisible(False)
self.ui.act_file_reload.setVisible(False)
self.ui.act_file_save_as.setVisible(False)
self.ui.tabUtils.removeTab(0)
else:
@@ -319,10 +327,12 @@ class HostWindow(QMainWindow):
self.ui.act_file_new.setEnabled(False)

self.ui.act_file_open.setEnabled(False)
self.ui.act_file_reload.setEnabled(False)
self.ui.act_file_save.setEnabled(False)
self.ui.act_file_save_as.setEnabled(False)
self.ui.act_engine_stop.setEnabled(False)
self.ui.act_plugin_remove_all.setEnabled(False)
# self.ui.act_plugin_remove_all.setEnabled(False)
self.setMenuMacrosEnabled(False)

self.ui.act_canvas_show_internal.setChecked(False)
self.ui.act_canvas_show_internal.setVisible(False)
@@ -503,6 +513,7 @@ class HostWindow(QMainWindow):
self.ui.act_file_refresh.setIcon(getIcon('view-refresh', 16, 'svgz'))
self.ui.act_file_new.setIcon(getIcon('document-new', 16, 'svgz'))
self.ui.act_file_open.setIcon(getIcon('document-open', 16, 'svgz'))
self.ui.act_file_reload.setIcon(getIcon('view-refresh-purple', 16, 'svgz'))
self.ui.act_file_save.setIcon(getIcon('document-save', 16, 'svgz'))
self.ui.act_file_save_as.setIcon(getIcon('document-save-as', 16, 'svgz'))
self.ui.act_file_quit.setIcon(getIcon('application-exit', 16, 'svgz'))
@@ -511,8 +522,18 @@ class HostWindow(QMainWindow):
self.ui.act_engine_panic.setIcon(getIcon('dialog-warning', 16, 'svgz'))
self.ui.act_engine_config.setIcon(getIcon('configure', 16, 'svgz'))
self.ui.act_plugin_add.setIcon(getIcon('list-add', 16, 'svgz'))
self.ui.act_plugin_add_jack.setIcon(getIcon('list-add', 16, 'svgz'))
self.ui.act_plugin_add_jack.setIcon(getIcon('add-jack', 16, 'svgz'))
self.ui.act_plugin_remove_all.setIcon(getIcon('edit-delete', 16, 'svgz'))
self.ui.act_plugins_enable.setIcon(getIcon('system-turnon', 16, 'svgz'))
self.ui.act_plugins_disable.setIcon(getIcon('system-shutdown', 16, 'svgz'))
self.ui.act_plugins_bypass.setIcon(getIcon('dry', 16, 'svgz'))
self.ui.act_plugins_wet100.setIcon(getIcon('wet', 16, 'svgz'))
self.ui.act_plugins_mute.setIcon(getIcon('audio-volume-muted', 16, 'svgz'))
self.ui.act_plugins_volume100.setIcon(getIcon('audio-volume-medium', 16, 'svgz'))
self.ui.act_plugins_center.setIcon(getIcon('balance', 16, 'svgz'))
self.ui.act_plugins_change_skin.setIcon(getIcon('skin', 16, 'svgz'))
self.ui.act_plugins_compact.setIcon(getIcon('compact', 16, 'svgz'))
self.ui.act_plugins_expand.setIcon(getIcon('restore', 16, 'svgz'))
self.ui.act_canvas_arrange.setIcon(getIcon('view-sort-ascending', 16, 'svgz'))
self.ui.act_canvas_refresh.setIcon(getIcon('view-refresh', 16, 'svgz'))
self.ui.act_canvas_zoom_fit.setIcon(getIcon('zoom-fit-best', 16, 'svgz'))
@@ -534,6 +555,7 @@ class HostWindow(QMainWindow):

self.ui.act_file_new.triggered.connect(self.slot_fileNew)
self.ui.act_file_open.triggered.connect(self.slot_fileOpen)
self.ui.act_file_reload.triggered.connect(self.slot_fileReload)
self.ui.act_file_save.triggered.connect(self.slot_fileSave)
self.ui.act_file_save_as.triggered.connect(self.slot_fileSaveAs)

@@ -553,10 +575,12 @@ class HostWindow(QMainWindow):
self.ui.act_plugins_wet100.triggered.connect(self.slot_pluginsWet100)
self.ui.act_plugins_bypass.triggered.connect(self.slot_pluginsBypass)
self.ui.act_plugins_center.triggered.connect(self.slot_pluginsCenter)
self.ui.act_plugins_change_skin.triggered.connect(self.slot_pluginsChangeSkin)
self.ui.act_plugins_compact.triggered.connect(self.slot_pluginsCompact)
self.ui.act_plugins_expand.triggered.connect(self.slot_pluginsExpand)

self.ui.act_settings_show_toolbar.toggled.connect(self.slot_showToolbar)
self.ui.act_settings_show_toolbar_text.toggled.connect(self.slot_showToolbarText)
self.ui.act_settings_show_meters.toggled.connect(self.slot_showCanvasMeters)
self.ui.act_settings_show_keyboard.toggled.connect(self.slot_showCanvasKeyboard)
self.ui.act_settings_show_side_panel.toggled.connect(self.slot_showSidePanel)
@@ -738,7 +762,7 @@ class HostWindow(QMainWindow):
self.host.set_custom_data(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaColor", colorStr)
pitem.recreateWidget(newColor = color)

def changePluginSkin(self, pluginId, skin):
def changePluginSkin(self, pluginId, skin, color = None):
if pluginId > self.fPluginCount:
return

@@ -748,7 +772,9 @@ class HostWindow(QMainWindow):
return

self.host.set_custom_data(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaSkin", skin)
if skin not in ("default","rncbc","presets","mpresets"):
if color is not None:
pitem.recreateWidget(newSkin = skin, newColor = color)
elif skin not in ("default","rncbc","presets","mpresets"):
pitem.recreateWidget(newSkin = skin, newColor = (255,255,255))
else:
pitem.recreateWidget(newSkin = skin)
@@ -935,6 +961,16 @@ class HostWindow(QMainWindow):
self.loadProjectNow()
self.fProjectFilename = filenameOld

if self.fTweaks.get('ShowReload', 0):
self.ui.act_file_reload.setEnabled(True)
self.ui.act_file_reload.setVisible(True)

@pyqtSlot()
def slot_fileReload(self):
if not (self.fProjectFilename == ""):
self.pluginRemoveAll()
self.loadProjectNow()

@pyqtSlot()
def slot_fileSave(self, saveAs=False):
if self.fProjectFilename and not saveAs:
@@ -1177,6 +1213,7 @@ class HostWindow(QMainWindow):

if self.host.isPlugin or not self.fSessionManagerName:
self.ui.act_file_open.setEnabled(False)
self.ui.act_file_reload.setEnabled(False)
self.ui.act_file_save_as.setEnabled(False)

@pyqtSlot(int, str)
@@ -1226,7 +1263,8 @@ class HostWindow(QMainWindow):
# Plugins

def removeAllPlugins(self):
self.ui.act_plugin_remove_all.setEnabled(False)
# self.ui.act_plugin_remove_all.setEnabled(False)
self.setMenuMacrosEnabled(False)
patchcanvas.handleAllPluginsRemoved()

while self.ui.listWidget.takeItem(0):
@@ -1343,6 +1381,12 @@ class HostWindow(QMainWindow):
act = fmenu.addAction(p['name'])
act.setData(p)
act.triggered.connect(self.slot_favoritePluginAdd)

if self.fSavedSettings[CARLA_KEY_MAIN_SYSTEM_ICONS]:
fmenu.setIcon(getIcon('add-from-favorites', 16, 'svgz'))
else:
fmenu.setIcon(QIcon(":/16x16/add-from-favorites.svgz"))

menu.addMenu(fmenu)

menu.addAction(self.ui.act_plugin_remove_all)
@@ -1359,6 +1403,7 @@ class HostWindow(QMainWindow):
menu.addSeparator()
menu.addAction(self.ui.act_plugins_center)
menu.addSeparator()
menu.addAction(self.ui.act_plugins_change_skin)
menu.addAction(self.ui.act_plugins_compact)
menu.addAction(self.ui.act_plugins_expand)

@@ -1454,7 +1499,7 @@ class HostWindow(QMainWindow):
if pitem is None:
break

pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 1.0)
pitem.getWidget().setInternalParameter(PARAMETER_VOLUME, 1.0)

@pyqtSlot()
def slot_pluginsMute(self):
@@ -1465,7 +1510,7 @@ class HostWindow(QMainWindow):
if pitem is None:
break

pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 0.0)
pitem.getWidget().setInternalParameter(PARAMETER_VOLUME, 0.0)

@pyqtSlot()
def slot_pluginsWet100(self):
@@ -1476,7 +1521,7 @@ class HostWindow(QMainWindow):
if pitem is None:
break

pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 1.0)
pitem.getWidget().setInternalParameter(PARAMETER_DRYWET, 1.0)

@pyqtSlot()
def slot_pluginsBypass(self):
@@ -1487,7 +1532,7 @@ class HostWindow(QMainWindow):
if pitem is None:
break

pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 0.0)
pitem.getWidget().setInternalParameter(PARAMETER_DRYWET, 0.0)

@pyqtSlot()
def slot_pluginsCenter(self):
@@ -1501,6 +1546,39 @@ class HostWindow(QMainWindow):
pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_LEFT, -1.0)
pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_RIGHT, 1.0)
pitem.getWidget().setInternalParameter(PARAMETER_PANNING, 0.0)
pitem.getWidget().setInternalParameter(PARAMETER_FORTH, 0.0)

@pyqtSlot()
def slot_pluginsChangeSkin(self):
skin = QInputDialog.getItem(self, self.tr("Change Skin"), self.tr("Change Skin to:"), skinList, 0, False)
if not all(skin):
return

if skin[0][:4] in ("calf", "clas", "zynf"): # These are non-colorable
for pluginId in range(self.fPluginCount):
gCarla.gui.changePluginSkin(pluginId, skin[0])
return

reColor = QInputDialog.getItem(self, self.tr("Change Color"), self.tr("Change Color mode:"), ['Follow Rules / As Is', 'Random'], 0, False)
if not all(reColor):
return

color = None
if skin[0].startswith("3ba"): # These are dark tinted, need enlight.
color = (254,255,255) # If not random
luma = 0.5
sat = 0.5
elif skin[0].startswith("ope"): # These are dark tinted, need enlight.
luma = 0.5
sat = 1.0
else:
luma = 0.125
sat = 0.25

for pluginId in range(self.fPluginCount):
if reColor[0][:2] == 'Ra':
color = QColor.fromHslF(random.random(), sat, luma, 1).getRgb()[0:3]
gCarla.gui.changePluginSkin(pluginId, skin[0], color)

@pyqtSlot()
def slot_pluginsCompact(self):
@@ -1531,11 +1609,24 @@ class HostWindow(QMainWindow):
self.fPluginList.append(pitem)
self.fPluginCount += 1

self.ui.act_plugin_remove_all.setEnabled(self.fPluginCount > 0)
self.setMenuMacrosEnabled(self.fPluginCount > 0)

if pluginType == PLUGIN_LV2:
self.fHasLoadedLv2Plugins = True

def setMenuMacrosEnabled(self, enabled):
self.ui.act_plugin_remove_all.setEnabled(enabled)
self.ui.act_plugins_enable.setEnabled(enabled)
self.ui.act_plugins_disable.setEnabled(enabled)
self.ui.act_plugins_volume100.setEnabled(enabled)
self.ui.act_plugins_mute.setEnabled(enabled)
self.ui.act_plugins_wet100.setEnabled(enabled)
self.ui.act_plugins_bypass.setEnabled(enabled)
self.ui.act_plugins_center.setEnabled(enabled)
self.ui.act_plugins_change_skin.setEnabled(enabled)
self.ui.act_plugins_compact.setEnabled(enabled)
self.ui.act_plugins_expand.setEnabled(enabled)

@pyqtSlot(int)
def slot_handlePluginRemovedCallback(self, pluginId):
if self.fWithCanvas:
@@ -1558,7 +1649,8 @@ class HostWindow(QMainWindow):
del pitem

if self.fPluginCount == 0:
self.ui.act_plugin_remove_all.setEnabled(False)
# self.ui.act_plugin_remove_all.setEnabled(False)
self.setMenuMacrosEnabled(False)
if self.fCurrentlyRemovingAllPlugins:
self.fCurrentlyRemovingAllPlugins = False
self.projectLoadingFinished(False)
@@ -1569,7 +1661,9 @@ class HostWindow(QMainWindow):
pitem = self.fPluginList[i]
pitem.setPluginId(i)

self.ui.act_plugin_remove_all.setEnabled(True)
# self.ui.act_plugin_remove_all.setEnabled(True)
self.setMenuMacrosEnabled(True)


# --------------------------------------------------------------------------------------------------------
# Canvas
@@ -1975,6 +2069,7 @@ class HostWindow(QMainWindow):

settings.setValue("Geometry", self.saveGeometry())
settings.setValue("ShowToolbar", self.ui.toolBar.isEnabled())
settings.setValue("ShowToolbarText", self.ui.act_settings_show_toolbar_text.isChecked())
settings.setValue("ShowSidePanel", self.ui.dockWidget.isEnabled())

diskFolders = []
@@ -2009,11 +2104,24 @@ class HostWindow(QMainWindow):

showToolbar = settings.value("ShowToolbar", True, bool)
self.ui.act_settings_show_toolbar.setChecked(showToolbar)
self.ui.act_settings_show_toolbar_text.setEnabled(showToolbar)
self.ui.toolBar.blockSignals(True)
self.ui.toolBar.setEnabled(showToolbar)
self.ui.toolBar.setVisible(showToolbar)
self.ui.toolBar.blockSignals(False)

showToolbarText = settings.value("ShowToolbarText", True, bool)
self.ui.act_settings_show_toolbar_text.setChecked(showToolbarText)
self.ui.toolBar.blockSignals(True)

for btn in self.ui.toolBar.findChildren(QToolButton):
if showToolbarText:
btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
else:
btn.setToolButtonStyle(Qt.ToolButtonIconOnly)

self.ui.toolBar.blockSignals(False)

#if settings.contains("SplitterState"):
#self.ui.splitter.restoreState(settings.value("SplitterState", b""))
#else:
@@ -2066,6 +2174,7 @@ class HostWindow(QMainWindow):
CARLA_KEY_MAIN_REFRESH_INTERVAL: settings.value(CARLA_KEY_MAIN_REFRESH_INTERVAL, CARLA_DEFAULT_MAIN_REFRESH_INTERVAL, int),
CARLA_KEY_MAIN_SYSTEM_ICONS: settings.value(CARLA_KEY_MAIN_SYSTEM_ICONS, CARLA_DEFAULT_MAIN_SYSTEM_ICONS, bool),
CARLA_KEY_MAIN_EXPERIMENTAL: settings.value(CARLA_KEY_MAIN_EXPERIMENTAL, CARLA_DEFAULT_MAIN_EXPERIMENTAL, bool),
CARLA_KEY_MAIN_SKIN_TWEAKS: settings.value(CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_DEFAULT_MAIN_SKIN_TWEAKS, str),
CARLA_KEY_CANVAS_THEME: settings.value(CARLA_KEY_CANVAS_THEME, CARLA_DEFAULT_CANVAS_THEME, str),
CARLA_KEY_CANVAS_SIZE: settings.value(CARLA_KEY_CANVAS_SIZE, CARLA_DEFAULT_CANVAS_SIZE, str),
CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS: settings.value(CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS, CARLA_DEFAULT_CANVAS_AUTO_HIDE_GROUPS, bool),
@@ -2182,6 +2291,19 @@ class HostWindow(QMainWindow):
self.ui.toolBar.blockSignals(True)
self.ui.toolBar.setEnabled(yesNo)
self.ui.toolBar.setVisible(yesNo)
self.ui.act_settings_show_toolbar_text.setEnabled(yesNo)
self.ui.toolBar.blockSignals(False)

@pyqtSlot(bool)
def slot_showToolbarText(self, yesNo):
self.ui.toolBar.blockSignals(True)

for btn in self.ui.toolBar.findChildren(QToolButton):
if yesNo:
btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
else:
btn.setToolButtonStyle(Qt.ToolButtonIconOnly)

self.ui.toolBar.blockSignals(False)

@pyqtSlot(bool)
@@ -2623,6 +2745,8 @@ class HostWindow(QMainWindow):
self.ui.toolBar.setEnabled(visible)
self.ui.toolBar.blockSignals(False)
self.ui.act_settings_show_toolbar.setChecked(visible)
self.ui.act_settings_show_toolbar_text.setEnabled(visible)


@pyqtSlot(int)
def slot_tabChanged(self, index):
@@ -2889,7 +3013,6 @@ class HostWindow(QMainWindow):

def idleFast(self):
self.host.engine_idle()
self.refreshTransport()

if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins:
return
@@ -2922,6 +3045,10 @@ class HostWindow(QMainWindow):
def idleSlow(self):
self.getAndRefreshRuntimeInfo()

self.slowTimer = (self.slowTimer + 1) % 4
if (self.slowTimer == 0):
self.refreshTransport() # This one is CPU hungry. Ticket #1934

if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins:
return

@@ -2949,6 +3076,13 @@ class HostWindow(QMainWindow):
QMainWindow.changeEvent(self, event)

def updateStyle(self):
loadTweaks(self)

if self.fTweaks.get('MoreSpace', 0):
self.ui.pad_left.hide()
self.ui.pad_right.hide()
return

# Rack padding images setup
rack_imgL = QImage(":/bitmaps/rack_padding_left.png")
rack_imgR = QImage(":/bitmaps/rack_padding_right.png")


+ 4
- 2
source/frontend/carla_host_control.py View File

@@ -189,6 +189,7 @@ class CarlaHostOSC(CarlaHostQtPlugin):
"set_balance_left",
"set_balance_right",
"set_panning",
"set_forth",
#"set_ctrl_channel",
"set_parameter_value",
"set_parameter_midi_channel",
@@ -426,17 +427,18 @@ class CarlaControlServerTCP(Server):
pluginId, index, type_, key, value = args
self.host._set_customData(pluginId, index, { 'type': type_, 'key': key, 'value': value })

@make_method('/ctrl/iparams', 'ifffffff')
@make_method('/ctrl/iparams', 'iffffffff')
def carla_iparams(self, path, args):
if DEBUG: print(path, args)
self.fReceivedMsgs = True
pluginId, active, drywet, volume, balLeft, balRight, pan, ctrlChan = args
pluginId, active, drywet, volume, balLeft, balRight, pan, ctrlChan, forth = args
self.host._set_internalValue(pluginId, PARAMETER_ACTIVE, active)
self.host._set_internalValue(pluginId, PARAMETER_DRYWET, drywet)
self.host._set_internalValue(pluginId, PARAMETER_VOLUME, volume)
self.host._set_internalValue(pluginId, PARAMETER_BALANCE_LEFT, balLeft)
self.host._set_internalValue(pluginId, PARAMETER_BALANCE_RIGHT, balRight)
self.host._set_internalValue(pluginId, PARAMETER_PANNING, pan)
self.host._set_internalValue(pluginId, PARAMETER_FORTH, forth)
self.host._set_internalValue(pluginId, PARAMETER_CTRL_CHANNEL, ctrlChan)

@make_method('/ctrl/resp', 'is')


+ 7
- 0
source/frontend/carla_settings.py View File

@@ -47,6 +47,7 @@ from carla_backend import (
from carla_shared import (
CARLA_KEY_MAIN_PROJECT_FOLDER,
CARLA_KEY_MAIN_USE_PRO_THEME,
CARLA_KEY_MAIN_SKIN_TWEAKS,
CARLA_KEY_MAIN_PRO_THEME_COLOR,
CARLA_KEY_MAIN_REFRESH_INTERVAL,
CARLA_KEY_MAIN_CONFIRM_EXIT,
@@ -115,6 +116,7 @@ from carla_shared import (
CARLA_DEFAULT_MAIN_CLASSIC_SKIN,
CARLA_DEFAULT_MAIN_SHOW_LOGS,
CARLA_DEFAULT_MAIN_SYSTEM_ICONS,
CARLA_DEFAULT_MAIN_SKIN_TWEAKS,
#CARLA_DEFAULT_MAIN_EXPERIMENTAL,
CARLA_DEFAULT_CANVAS_THEME,
CARLA_DEFAULT_CANVAS_SIZE,
@@ -705,6 +707,9 @@ class CarlaSettingsW(QDialog):
self.ui.cb_main_theme_color.findText(settings.value(CARLA_KEY_MAIN_PRO_THEME_COLOR,
CARLA_DEFAULT_MAIN_PRO_THEME_COLOR, str)))

self.ui.le_main_skin_tweaks.setText(
settings.value(CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_DEFAULT_MAIN_SKIN_TWEAKS, str))

self.ui.sb_main_refresh_interval.setValue(
settings.value(CARLA_KEY_MAIN_REFRESH_INTERVAL, CARLA_DEFAULT_MAIN_REFRESH_INTERVAL, int))

@@ -995,6 +1000,7 @@ class CarlaSettingsW(QDialog):
settings.setValue(CARLA_KEY_MAIN_CONFIRM_EXIT, self.ui.ch_main_confirm_exit.isChecked())
settings.setValue(CARLA_KEY_MAIN_CLASSIC_SKIN, self.ui.cb_main_classic_skin_default.isChecked())
settings.setValue(CARLA_KEY_MAIN_USE_PRO_THEME, self.ui.ch_main_theme_pro.isChecked())
settings.setValue(CARLA_KEY_MAIN_SKIN_TWEAKS, self.ui.le_main_skin_tweaks.text())
settings.setValue(CARLA_KEY_MAIN_PRO_THEME_COLOR, self.ui.cb_main_theme_color.currentText())
settings.setValue(CARLA_KEY_MAIN_REFRESH_INTERVAL, self.ui.sb_main_refresh_interval.value())
settings.setValue(CARLA_KEY_MAIN_SYSTEM_ICONS, self.ui.ch_main_system_icons.isChecked())
@@ -1193,6 +1199,7 @@ class CarlaSettingsW(QDialog):
self.ui.group_main_theme.isEnabled())
self.ui.cb_main_theme_color.setCurrentIndex(
self.ui.cb_main_theme_color.findText(CARLA_DEFAULT_MAIN_PRO_THEME_COLOR))
self.ui.le_main_skin_tweaks.setText(CARLA_DEFAULT_MAIN_SKIN_TWEAKS)
self.ui.sb_main_refresh_interval.setValue(CARLA_DEFAULT_MAIN_REFRESH_INTERVAL)
self.ui.ch_main_confirm_exit.setChecked(CARLA_DEFAULT_MAIN_CONFIRM_EXIT)
self.ui.cb_main_classic_skin_default(CARLA_DEFAULT_MAIN_CLASSIC_SKIN)


+ 57
- 8
source/frontend/carla_shared.py View File

@@ -8,7 +8,8 @@
import os
import sys

from math import fmod
from math import fmod, log10
import numpy as np

# ------------------------------------------------------------------------------------------------------------
# Imports (Signal)
@@ -63,6 +64,9 @@ from carla_backend import (
ENGINE_TRANSPORT_MODE_JACK,
)

from utils import QSafeSettings
import ast

# ------------------------------------------------------------------------------------------------------------
# Config

@@ -184,6 +188,7 @@ CANVAS_EYECANDY_SMALL = 1
CARLA_KEY_MAIN_PROJECT_FOLDER = "Main/ProjectFolder" # str
CARLA_KEY_MAIN_USE_PRO_THEME = "Main/UseProTheme" # bool
CARLA_KEY_MAIN_PRO_THEME_COLOR = "Main/ProThemeColor" # str
CARLA_KEY_MAIN_SKIN_TWEAKS = "Main/SkinTweaks" # str
CARLA_KEY_MAIN_REFRESH_INTERVAL = "Main/RefreshInterval" # int
CARLA_KEY_MAIN_CONFIRM_EXIT = "Main/ConfirmExit" # bool
CARLA_KEY_MAIN_CLASSIC_SKIN = "Main/ClassicSkin" # bool
@@ -273,6 +278,7 @@ CARLA_DEFAULT_MAIN_CLASSIC_SKIN = False
CARLA_DEFAULT_MAIN_SHOW_LOGS = bool(not CARLA_OS_WIN)
CARLA_DEFAULT_MAIN_SYSTEM_ICONS = False
CARLA_DEFAULT_MAIN_EXPERIMENTAL = False
CARLA_DEFAULT_MAIN_SKIN_TWEAKS = "'ShowPan':0, 'ShowForth':0, 'WetVolPush':0, 'WetVolPushLed':1, 'Tooltips':0, 'MoreSpace':0, 'WetVolOnCompact':0, 'SymmetricArc':1, 'GapAuto':0, 'ColorFollow':0, 'ShortenLabels':1, 'ButtonHaveLed':1, 'ColoredNeon':1, 'HighContrast':0, 'ShowDisabled':0, 'ShowOutputs':1, 'ShowButtons':1, 'Button3Pos':1, 'TwoLineLabels':0, 'GapMin':0, 'GapMax':100, 'ColorFrom':-0.1, 'ColorSpan':0.4, 'Auto7segSize':0, 'Auto7segWidth':1, 'ShowReload':0, 'ShowPrograms':0, 'ShowMidiPrograms':0, 'ShowProgramsOnCompact':0, 'ShowMidiProgramsOnCompact':0, "

# Canvas
CARLA_DEFAULT_CANVAS_THEME = "Modern Dark"
@@ -647,18 +653,22 @@ else:
# Find decimal points for a parameter, using step and stepSmall

def countDecimalPoints(step, stepSmall):
if stepSmall >= 1.0:
if (stepSmall >= 1.0) or (step <= 0) or (stepSmall <= 0):
return 0
if step >= 1.0:
return 2

count = 0
value = fmod(abs(step), 1)
while 0.0001 < value < 0.999 and count < 6:
value = fmod(value*10, 1)
count += 1
# OLD
# count = 0
# value = fmod(abs(step), 1)
# while 0.0001 < value < 0.999 and count < 6:
# value = fmod(value*10, 1)
# count += 1
#
# return count

return count
# NEW: Looks like better handling of small values.
return -int(log10(stepSmall)) + 2

# ------------------------------------------------------------------------------------------------------------
# Check if a value is a number (float support)
@@ -927,5 +937,44 @@ def CustomMessageBox(parent, icon, title, text,
# pylint: enable=no-value-for-parameter
# pylint: enable=too-many-arguments

# ------------------------------------------------------------------------------------------------------------
# Tweaks, in form of 'Parameter':Value or 'skinnameParameter':Value, are holds both per-rack and per-plugin fine-tuning values (tweaks).

def loadTweaks(self):
settings = QSafeSettings("falkTX", "Carla2")
skinTweaks = settings.value(CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_DEFAULT_MAIN_SKIN_TWEAKS, str)
try:
self.fTweaks = ast.literal_eval('{' + skinTweaks + '}')
except ValueError as e:
print("ERROR while parse `" + skinTweaks + "` :", e)

# ------------------------------------------------------------------------------------------------------------

def getPrefixSuffix(unit):
prefix = ""
suffix = unit.strip()

if suffix == "(coef)":
prefix = "* "
suffix = ""
else:
suffix = " " + suffix

return prefix, suffix

# ------------------------------------------------------------------------------------------------------------

def strLim(value, digits = 5):
result = np.format_float_positional(value, trim='-', fractional=False, precision=digits)
if len(result) > 9:
return '{:.3e}'.format(value)
else:
return result

# ------------------------------------------------------------------------------------------------------------
# Geometry

RACK_KNOB_GAP = 5

# ------------------------------------------------------------------------------------------------------------
# pylint: enable=possibly-used-before-assignment

+ 406
- 203
source/frontend/carla_skin.py View File

@@ -6,15 +6,19 @@
# Imports (Global)

from qt_compat import qt_config
import math
import random
import operator
from operator import itemgetter

if qt_config == 5:
from PyQt5.QtCore import Qt, QRectF, QLineF, QTimer
from PyQt5.QtGui import QColor, QFont, QFontDatabase, QPainter, QPainterPath, QPen
from PyQt5.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton
from PyQt5.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton, QComboBox, QSizePolicy
elif qt_config == 6:
from PyQt6.QtCore import Qt, QRectF, QLineF, QTimer
from PyQt6.QtGui import QColor, QFont, QFontDatabase, QPainter, QPainterPath, QPen
from PyQt6.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton
from PyQt6.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton, QComboBox, QSizePolicy

# ------------------------------------------------------------------------------------------------------------
# Imports (Custom)
@@ -29,7 +33,6 @@ from carla_backend import *
from carla_shared import *
from carla_widgets import *
from widgets.digitalpeakmeter import DigitalPeakMeter
from widgets.paramspinbox import CustomInputDialog
from widgets.scalabledial import ScalableDial

# ------------------------------------------------------------------------------------------------------------
@@ -126,7 +129,7 @@ def getParameterShortName(paramName):
# Get RGB colors for a plugin category

def getColorFromCategory(category):
r = 40
r = 39
g = 40
b = 40

@@ -152,45 +155,35 @@ def getColorFromCategory(category):
return (r, g, b)

# ------------------------------------------------------------------------------------------------------------
#

def setScalableDialStyle(widget, parameterId, parameterCount, whiteLabels, skinStyle):
if skinStyle.startswith("calf"):
widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_NO_GRADIENT)
widget.setImage(7)

elif skinStyle.startswith("openav"):
widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_NO_GRADIENT)
if parameterId == PARAMETER_DRYWET:
widget.setImage(13)
elif parameterId == PARAMETER_VOLUME:
widget.setImage(12)
else:
widget.setImage(11)

else:
if parameterId == PARAMETER_DRYWET:
widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET)

elif parameterId == PARAMETER_VOLUME:
widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL)

else:
_r = 255 - int((float(parameterId)/float(parameterCount))*200.0)
_g = 55 + int((float(parameterId)/float(parameterCount))*200.0)
_b = 0 #(r-40)*4
widget.setCustomPaintColor(QColor(_r, _g, _b))
widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_COLOR)

if whiteLabels:
colorEnabled = QColor("#BBB")
colorDisabled = QColor("#555")
else:
colorEnabled = QColor("#111")
colorDisabled = QColor("#AAA")

widget.setLabelColor(colorEnabled, colorDisabled)
widget.setImage(3)
skinList = [
"default",
"3bandeq",
"rncbc",
"calf_black",
"calf_blue",
"classic",
"openav-old",
"openav",
"zynfx",
"presets",
"mpresets",
"tube",
]

skinListTweakable = [
"default",
"calf",
"openav",
"zynfx",
"tube",
]

def arrayIndex(array, value):
for index, item in enumerate(array):
if item.startswith(value):
return index
return 0

# ------------------------------------------------------------------------------------------------------------
# Abstract plugin slot
@@ -235,6 +228,16 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):

self.fAdjustViewableKnobCountScheduled = False

# load fresh skin tweaks
self.fTweaks = {}
loadTweaks(self)

# take panel color hue & sat to make "follow panel" paint
color = QColor(skinColor[0], skinColor[1], skinColor[2])
hue = color.hueF() % 1.0
sat = color.saturationF()
self.fColorHint = int(hue * 100) + int(sat * 100) / 100.0 # 50.80: 50% hue, 80% sat

# used during testing
self.fIdleTimerId = 0

@@ -269,6 +272,8 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
self.w_knobs_right = None
self.spacer_knobs = None

self.slowTimer = 0

# -------------------------------------------------------------
# Set-up connections

@@ -348,8 +353,31 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
if self.fEditDialog is not None and self.fPluginId == pluginId:
self.customUiStateChanged(state)

# @pyqtSlot(int, int, int)
# def slot_handleParameterKnobVisible(self, pluginId, index, value):
# if self.fEditDialog is not None and self.fPluginId == pluginId:
# self.setKnobVisible(index, value)

# @pyqtSlot(bool)
# def slot_knobVisible(self, value):
# self.host.set_drywet(self.fPluginId, value)
# self.setParameterValue(PARAMETER_DRYWET, value, True)

@pyqtSlot(float)
def slot_dryWetChanged(self, value):
self.host.set_drywet(self.fPluginId, value)
self.setParameterValue(PARAMETER_DRYWET, value, True)

@pyqtSlot(float)
def slot_volumeChanged(self, value):
self.host.set_volume(self.fPluginId, value)
self.setParameterValue(PARAMETER_VOLUME, value, True)

# ------------------------------------------------------------------

def tweak(self, skinName, tweakName, default):
return self.fTweaks.get(skinName + tweakName, self.fTweaks.get(tweakName, default))

def ready(self):
self.fIsActive = bool(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_ACTIVE) >= 0.5)

@@ -481,6 +509,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
self.peak_in.setMeterStyle(DigitalPeakMeter.STYLE_RNCBC)
elif self.fSkinStyle.startswith("openav") or self.fSkinStyle == "zynfx":
self.peak_in.setMeterStyle(DigitalPeakMeter.STYLE_OPENAV)
elif self.fSkinStyle == "tube":
self.peak_in.setMeterStyle(DigitalPeakMeter.STYLE_TUBE)
self.peak_in.setMeterLinesEnabled(False)

if self.fPeaksInputCount == 0 and not isinstance(self, PluginSlot_Classic):
self.peak_in.hide()
@@ -496,6 +527,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
self.peak_out.setMeterStyle(DigitalPeakMeter.STYLE_RNCBC)
elif self.fSkinStyle.startswith("openav") or self.fSkinStyle == "zynfx":
self.peak_out.setMeterStyle(DigitalPeakMeter.STYLE_OPENAV)
elif self.fSkinStyle == "tube":
self.peak_out.setMeterStyle(DigitalPeakMeter.STYLE_TUBE)
self.peak_out.setMeterLinesEnabled(False)

if self.fPeaksOutputCount == 0 and not isinstance(self, PluginSlot_Classic):
self.peak_out.hide()
@@ -530,7 +564,8 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
styleSheet2 = "background-image: url(:/bitmaps/background_%s.png);" % self.fSkinStyle
else:
styleSheet2 = "background-color: rgb(200, 200, 200);"
styleSheet2 += "background-image: url(:/bitmaps/background_noise1.png);"
if self.fSkinStyle not in ("classic"):
styleSheet2 += "background-image: url(:/bitmaps/background_noise1.png);"

if not self.fDarkStyle:
colorEnabled = "#111"
@@ -551,12 +586,63 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):

styleSheet += """
QComboBox#cb_presets,
QComboBox#cb_presets0,
QComboBox#cb_presets1,
QLabel#label_audio_in,
QLabel#label_audio_out,
QLabel#label_midi { font-size: 10px; }
"""
self.setStyleSheet(styleSheet)

# -------------------------------------------------------------
# Wet and Vol knobs on compacted slot

# If "long" style not in "shorts" list, it will be matched to 0 ("default").
skinNum = arrayIndex(skinListTweakable, self.fSkinStyle[: 3])
skinName = skinListTweakable [skinNum]

wetVolOnCompact = self.tweak(skinName, 'WetVolOnCompact', 0)
showDisabled = self.tweak(skinName, 'ShowDisabled', 0)
showOutputs = self.tweak(skinName, 'ShowOutputs', 0)
shortenLabels = self.tweak(skinName, 'ShortenLabels', 1)
btn3state = self.tweak(skinName, 'Button3Pos', 1)

if isinstance(self, PluginSlot_Compact):
if self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET or showDisabled:


self.dial0 = ScalableDial(self, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET_MINI, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks)
self.dial0.setObjectName("dial0")
self.ui.horizontalLayout_2.insertWidget(6, self.dial0)

if wetVolOnCompact:
self.dial0.setEnabled(bool(self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET))
self.dial0.dragStateChanged.connect(self.slot_parameterDragStateChanged)
self.dial0.realValueChanged.connect(self.slot_dryWetChanged)
self.dial0.customContextMenuRequested.connect(self.slot_knobCustomMenu)
self.dial0.blockSignals(True)
self.dial0.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_DRYWET))
self.dial0.blockSignals(False)
else:
self.dial0.hide()

if self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME or showDisabled:
# self.dial1 = ScalableDial(self.ui.dial1, PARAMETER_VOLUME, 254, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL_MINI, 0, "%", self.fSkinStyle, whiteLabels, self.fTweaks)
self.dial1 = ScalableDial(self, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL_MINI, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks)
self.dial1.setObjectName("dial1")
self.ui.horizontalLayout_2.insertWidget(6, self.dial1)

if wetVolOnCompact:
self.dial1.setEnabled(bool(self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME))
self.dial1.dragStateChanged.connect(self.slot_parameterDragStateChanged)
self.dial1.realValueChanged.connect(self.slot_volumeChanged)
self.dial1.customContextMenuRequested.connect(self.slot_knobCustomMenu)
self.dial1.blockSignals(True)
self.dial1.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_VOLUME))
self.dial1.blockSignals(False)
else:
self.dial1.hide()

# -------------------------------------------------------------
# Set-up parameters

@@ -565,6 +651,11 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):

index = 0
layout = self.w_knobs_left.layout()

# Rainbow paint, default is deep red -> green. Span can be negative.
hueFrom = self.tweak(skinName, 'ColorFrom', -0.03)
hueSpan = self.tweak(skinName, 'ColorSpan', 0.4)

for i in range(parameterCount):
# 50 should be enough for everybody, right?
if index >= 50:
@@ -573,64 +664,153 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
paramInfo = self.host.get_parameter_info(self.fPluginId, i)
paramData = self.host.get_parameter_data(self.fPluginId, i)
paramRanges = self.host.get_parameter_ranges(self.fPluginId, i)
isInteger = (paramData['hints'] & PARAMETER_IS_INTEGER) != 0
default = self.host.get_default_parameter_value(self.fPluginId, i)
minimum = paramRanges['min']
maximum = paramRanges['max']
isEnabled = (paramData['hints'] & PARAMETER_IS_ENABLED) != 0
isOutput = (paramData['type'] != PARAMETER_INPUT)
isBoolean = (paramData['hints'] & PARAMETER_IS_BOOLEAN) != 0
isInteger = ((paramData['hints'] & PARAMETER_IS_INTEGER) != 0) or isBoolean

if paramData['type'] != PARAMETER_INPUT:
continue
if paramData['hints'] & PARAMETER_IS_BOOLEAN:
continue
if (paramData['hints'] & PARAMETER_IS_ENABLED) == 0:
continue
if (paramData['hints'] & PARAMETER_USES_SCALEPOINTS) != 0 and not isInteger:
# NOTE: we assume integer scalepoints are continuous
continue
if isInteger and paramRanges['max']-paramRanges['min'] <= 3:
continue
if paramInfo['name'].startswith("unused"):
print("Carla: INFO: Parameter "+str(i)+" is Unused, so skipped.")
continue

paramName = getParameterShortName(paramInfo['name'])
if not isEnabled:
print("Carla: INFO: Parameter "+str(i)+" is Disabled.")
if not showDisabled:
continue

delta = maximum - minimum
if delta <= 0:
print("Carla: ERROR: Parameter "+str(i)+": Min, Max are same or wrong.")
return

# NOTE: Booleans are mimic as isInteger with range [0 or 1].
if btn3state:
isButton = (isInteger and (minimum == 0) and (maximum in (1, 2)))
else:
isButton = (isInteger and (minimum == 0) and (maximum == 1))

vuMeter = 0
precision = 1
if isOutput:
if not showOutputs:
continue
vuMeter = ((minimum == 0) and ((maximum == 1) or (maximum == 100)))\
or (minimum == -maximum) # from -N to N, is it good to use VU ?
else:
# Integers have somewhat more coarse step
if isInteger:
while delta > 50:
delta = int(math.ceil(delta / 2))
precision = delta

# Floats are finer-step smoothed
else:
# Pretty steps for most common values, like 1-2-5-10 scales,
# still not in its final form.
while delta > 200:
# Mantissa is near 2.5
is25 = int(abs((log10(delta) % 1) - log10(2.5)) < 0.001)
delta = delta / (2.0 + is25 * 0.5)

while delta < 100:
# Mantissa is near 2.0
is25 = int(abs((log10(delta) % 1) - log10(2.0)) < 0.001)
delta = delta * (2.0 + is25 * 0.5)

precision = math.ceil(delta)

if precision <= 0: # suddenly...
print("Carla: ERROR: Parameter "+str(i)+": Precision "+str(precision)+" is wrong!")
return

if shortenLabels:
label = getParameterShortName(paramInfo['name'])
else:
label = paramInfo['name']

widget = ScalableDial(self, i,
precision,
default,
minimum,
maximum,
label,
skinNum * 16,
self.fColorHint,
paramInfo['unit'],
self.fSkinStyle,
whiteLabels,
self.fTweaks,
isInteger,
isButton,
isOutput,
vuMeter,
1 ) # isVisible Experiment (index % 2)

widget.setEnabled(isEnabled)

widget = ScalableDial(self, i)
widget.setLabel(paramName)
widget.setMinimum(paramRanges['min'])
widget.setMaximum(paramRanges['max'])
widget.hide()

if isInteger:
widget.setPrecision(paramRanges['max']-paramRanges['min'], True)
scalePoints = []
prefix = ""
suffix = ""
# NOTE: Issue #1983
# if ((paramData['hints'] & PARAMETER_USES_SCALEPOINTS) != 0):
count = paramInfo['scalePointCount']
if count:
for j in range(count):
scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, i, j))

setScalableDialStyle(widget, i, parameterCount, whiteLabels, self.fSkinStyle)
prefix, suffix = getPrefixSuffix(paramInfo['unit'])
widget.setScalePPS(sorted(scalePoints, key=operator.itemgetter("value")), prefix, suffix)

index += 1
self.fParameterList.append([i, widget])
layout.addWidget(widget)

if self.w_knobs_right is not None and (self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET) != 0:
widget = ScalableDial(self, PARAMETER_DRYWET)
widget.setLabel("Dry/Wet")
widget.setMinimum(0.0)
widget.setMaximum(1.0)
setScalableDialStyle(widget, PARAMETER_DRYWET, 0, whiteLabels, self.fSkinStyle)
for i in range(index):
widget = layout.itemAt(i).widget()
if widget is not None:
coef = i/(index-1) if index > 1 else 0.5 # 0.5 = Midrange
hue = (hueFrom + coef * hueSpan) % 1.0
widget.setCustomPaintColor(QColor.fromHslF(hue, 1, 0.5, 1))

self.fParameterList.append([PARAMETER_DRYWET, widget])
self.w_knobs_right.layout().addWidget(widget)

if self.w_knobs_right is not None and (self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME) != 0:
widget = ScalableDial(self, PARAMETER_VOLUME)
widget.setLabel("Volume")
widget.setMinimum(0.0)
widget.setMaximum(1.27)
setScalableDialStyle(widget, PARAMETER_VOLUME, 0, whiteLabels, self.fSkinStyle)
if self.w_knobs_right is not None:
if (self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET) != 0:
widget = ScalableDial(self, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks)

self.fParameterList.append([PARAMETER_DRYWET, widget])
self.w_knobs_right.layout().addWidget(widget)

if (self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME) != 0:
widget = ScalableDial(self, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks)

self.fParameterList.append([PARAMETER_VOLUME, widget])
self.w_knobs_right.layout().addWidget(widget)

if (self.fPluginInfo['hints'] & PLUGIN_CAN_PANNING) != 0:
if widget.getTweak('ShowPan', 0):
widget = ScalableDial(self, PARAMETER_PANNING, 100, 0.0, -1.0, 1.0, "Pan", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_PAN, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks)

self.fParameterList.append([PARAMETER_PANNING, widget])
self.w_knobs_right.layout().addWidget(widget)

if (self.fPluginInfo['hints'] & PLUGIN_CAN_FORTH) != 0:
if widget.getTweak('ShowForth', 0):
widget = ScalableDial(self, PARAMETER_FORTH, 100, 0.0, -1.0, 1.0, "Forth", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_FORTH, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks)

self.fParameterList.append([PARAMETER_FORTH, widget])
self.w_knobs_right.layout().addWidget(widget)

self.fParameterList.append([PARAMETER_VOLUME, widget])
self.w_knobs_right.layout().addWidget(widget)

for paramIndex, paramWidget in self.fParameterList:
paramWidget.setContextMenuPolicy(Qt.CustomContextMenu)
paramWidget.customContextMenuRequested.connect(self.slot_knobCustomMenu)
paramWidget.dragStateChanged.connect(self.slot_parameterDragStateChanged)
paramWidget.realValueChanged.connect(self.slot_parameterValueChanged)
if not paramWidget.fIsOutput:
paramWidget.customContextMenuRequested.connect(self.slot_knobCustomMenu)
paramWidget.dragStateChanged.connect(self.slot_parameterDragStateChanged)
paramWidget.realValueChanged.connect(self.slot_parameterValueChanged)
paramWidget.blockSignals(True)
paramWidget.setValue(self.host.get_internal_parameter_value(self.fPluginId, paramIndex))
paramWidget.blockSignals(False)
@@ -721,9 +901,14 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
if (self.fPluginInfo['hints'] & PLUGIN_CAN_PANNING) == 0: return
self.host.set_panning(self.fPluginId, value)

elif parameterId == PARAMETER_FORTH:
if (self.fPluginInfo['hints'] & PLUGIN_CAN_FORTH) == 0: return
self.host.set_forth(self.fPluginId, value)

elif parameterId == PARAMETER_CTRL_CHANNEL:
self.host.set_ctrl_channel(self.fPluginId, value)

self.setParameterValue(parameterId, value, True)
self.fEditDialog.setParameterValue(parameterId, value)

# -----------------------------------------------------------------
@@ -891,13 +1076,18 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
paramWidget.setVisible(hints & PLUGIN_CAN_DRYWET)
elif paramIndex == PARAMETER_VOLUME:
paramWidget.setVisible(hints & PLUGIN_CAN_VOLUME)
# jpka: FIXME i add it, but can't trigger it for test, so disable to prevent possible crashes. Maybe it don't needed.
# self.dial0.setVisible(hints & PLUGIN_CAN_DRYWET)
# self.dial1.setVisible(hints & PLUGIN_CAN_VOLUME)
# print("self.dial0.setVisible(hints & PLUGIN_CAN_DRYWET)")

if self.b_gui is not None:
self.b_gui.setEnabled(bool(hints & PLUGIN_HAS_CUSTOM_UI))

# NOTE: self.fParameterList is empty when compacted.
def editDialogParameterValueChanged(self, pluginId, parameterId, value):
for paramIndex, paramWidget in self.fParameterList:
if paramIndex != parameterId:
if (paramIndex != parameterId) or paramWidget.fIsOutput:
continue

paramWidget.blockSignals(True)
@@ -905,6 +1095,16 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
paramWidget.blockSignals(False)
break

if isinstance(self, PluginSlot_Compact):
if (parameterId == PARAMETER_DRYWET) and (self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET):
self.dial0.blockSignals(True)
self.dial0.setValue(value)
self.dial0.blockSignals(False)
if (parameterId == PARAMETER_VOLUME) and (self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME):
self.dial1.blockSignals(True)
self.dial1.setValue(value)
self.dial1.blockSignals(False)

def editDialogProgramChanged(self, pluginId, index):
if self.cb_presets is None:
return
@@ -997,6 +1197,23 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):

self.fEditDialog.idleSlow()

# 7-seg displays are pretty effective, but added frame skip will make it even better.
self.slowTimer = (self.slowTimer + 1) % 2 # Half the FPS, win some CPU.

# NOTE: self.fParameterList is empty when compacted.
for paramIndex, paramWidget in self.fParameterList:
if not paramWidget.fIsOutput:
continue
# VU displays are CPU effective, make it run faster than 7-seg displays.
# if (self.slowTimer > 0) and (not paramWidget.fIsVuOutput):
if (self.slowTimer > 0):
continue

paramWidget.blockSignals(True) # TODO Is it required for output?
value = self.host.get_current_parameter_value(self.fPluginId, paramIndex)
paramWidget.setValue(value, False)
paramWidget.blockSignals(False)

# -----------------------------------------------------------------

def drawOutline(self, painter):
@@ -1023,7 +1240,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):

def updateParameterValues(self):
for paramIndex, paramWidget in self.fParameterList:
if paramIndex < 0:
if paramIndex < 0: # DryWet and Volume
continue
if paramWidget.fIsOutput:
continue

paramWidget.blockSignals(True)
@@ -1044,8 +1263,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
# Expand/Minimize and Tweaks

actCompact = menu.addAction(self.tr("Expand") if isinstance(self, PluginSlot_Compact) else self.tr("Minimize"))
actColor = menu.addAction(self.tr("Change Color..."))
actSkin = menu.addAction(self.tr("Change Skin..."))
actColor = menu.addAction(self.tr("Change Color..."))
actColorRandom = menu.addAction(self.tr("Random Color"))
actSkin = menu.addAction(self.tr("Change Skin..."))
menu.addSeparator()

# -------------------------------------------------------------
@@ -1157,28 +1377,17 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
colorStr = "%i;%i;%i" % color
gCarla.gui.changePluginColor(self.fPluginId, color, colorStr)

elif actSel == actSkin:
skinList = [
"default",
"3bandeq",
"rncbc",
"calf_black",
"calf_blue",
"classic",
"openav-old",
"openav",
"zynfx",
"presets",
"mpresets",
]
try:
index = skinList.index(self.fSkinStyle)
except:
index = 0
elif actSel == actColorRandom:
hue = QColor(self.fSkinColor[0], self.fSkinColor[1], self.fSkinColor[2]).hueF()
color = QColor.fromHslF((hue + random.random()*0.5 + 0.25) % 1.0, 0.25, 0.125, 1).getRgb()[0:3]
colorStr = "%i;%i;%i" % color
gCarla.gui.changePluginColor(self.fPluginId, color, colorStr)

elif actSel == actSkin:
skin = QInputDialog.getItem(self, self.tr("Change Skin"),
self.tr("Change Skin to:"),
skinList, index, False)
skinList, arrayIndex(skinList, self.fSkinStyle), False)

if not all(skin):
return
gCarla.gui.changePluginSkin(self.fPluginId, skin[0])
@@ -1287,97 +1496,7 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):

@pyqtSlot()
def slot_knobCustomMenu(self):
sender = self.sender()
index = sender.fIndex
minimum = sender.fMinimum
maximum = sender.fMaximum
current = sender.fRealValue
label = sender.fLabel

if index in (PARAMETER_NULL, PARAMETER_CTRL_CHANNEL) or index <= PARAMETER_MAX:
return
elif index in (PARAMETER_DRYWET, PARAMETER_VOLUME):
default = 1.0
elif index == PARAMETER_BALANCE_LEFT:
default = -1.0
elif index == PARAMETER_BALANCE_RIGHT:
default = 1.0
elif index == PARAMETER_PANNING:
default = 0.0
else:
default = self.host.get_default_parameter_value(self.fPluginId, index)

if index < PARAMETER_NULL:
# show in integer percentage
textReset = self.tr("Reset (%i%%)" % round(default*100.0))
textMinim = self.tr("Set to Minimum (%i%%)" % round(minimum*100.0))
textMaxim = self.tr("Set to Maximum (%i%%)" % round(maximum*100.0))
else:
# show in full float value
textReset = self.tr("Reset (%f)" % default)
textMinim = self.tr("Set to Minimum (%f)" % minimum)
textMaxim = self.tr("Set to Maximum (%f)" % maximum)

menu = QMenu(self)
actReset = menu.addAction(textReset)
menu.addSeparator()
actMinimum = menu.addAction(textMinim)
actCenter = menu.addAction(self.tr("Set to Center"))
actMaximum = menu.addAction(textMaxim)
menu.addSeparator()
actSet = menu.addAction(self.tr("Set value..."))

if index > PARAMETER_NULL or index not in (PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING):
menu.removeAction(actCenter)

actSelected = menu.exec_(QCursor.pos())

if actSelected == actSet:
if index < PARAMETER_NULL:
value, ok = QInputDialog.getInt(self, self.tr("Set value"), label, round(current*100), round(minimum*100), round(maximum*100), 1)

if not ok:
return

value = float(value)/100.0

else:
paramInfo = self.host.get_parameter_info(self.fPluginId, index)
paramRanges = self.host.get_parameter_ranges(self.fPluginId, index)
scalePoints = []

for i in range(paramInfo['scalePointCount']):
scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, index, i))

prefix = ""
suffix = paramInfo['unit'].strip()

if suffix == "(coef)":
prefix = "* "
suffix = ""
else:
suffix = " " + suffix

dialog = CustomInputDialog(self, label, current, minimum, maximum,
paramRanges['step'], paramRanges['stepSmall'], scalePoints, prefix, suffix)

if not dialog.exec_():
return

value = dialog.returnValue()

elif actSelected == actMinimum:
value = minimum
elif actSelected == actMaximum:
value = maximum
elif actSelected == actReset:
value = default
elif actSelected == actCenter:
value = 0.0
else:
return

sender.setValue(value, True)
PluginEdit.slot_knobCustomMenu(self)

# -----------------------------------------------------------------

@@ -1439,9 +1558,23 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
if index < 0:
break

curWidth += widget.width() + 4
if not widget.getIsVisible():
continue

curWidth += widget.width() + RACK_KNOB_GAP

if curWidth + widget.width() * 2 + 8 < maxWidth:
if self.fTweaks.get('MoreSpace', 0):
if self.w_knobs_right is None: # calf
limit = curWidth
else:
if QT_VERSION < 0x60000:
limit = curWidth + self.w_knobs_right.getContentsMargins()[0] + 8
else:
limit = curWidth + 4 + 8
else:
limit = curWidth + 56 + 8

if limit < maxWidth:
#if not widget.isVisible():
widget.show()
continue
@@ -1719,6 +1852,12 @@ class PluginSlot_Compact(AbstractPluginSlot):
self.peak_in = self.ui.peak_in
self.peak_out = self.ui.peak_out

if self.fTweaks.get('ShowProgramsOnCompact', 0):
insertProgramList(self, self.ui.layout_peaks, 0)

if self.fTweaks.get('ShowMidiProgramsOnCompact', 0):
insertMidiProgramList(self, self.ui.layout_peaks, 0)

self.ready()

# -----------------------------------------------------------------
@@ -1756,11 +1895,24 @@ class PluginSlot_Default(AbstractPluginSlot):
self.w_knobs_right = self.ui.w_knobs_right
self.spacer_knobs = self.ui.layout_bottom.itemAt(1).spacerItem()

if self.fTweaks.get('MoreSpace', 0):
self.ui.layout_bottom.setContentsMargins(0, 4, 0, 0)

if self.fTweaks.get('ShowPrograms', 0):
# insertProgramList(self, self.ui.layout_top, 6)
insertProgramList(self, self.ui.layout_peaks, 0)

if self.fTweaks.get('ShowMidiPrograms', 0):
# insertMidiProgramList(self, self.ui.layout_top, 6)
insertMidiProgramList(self, self.ui.layout_peaks, 0)

self.ready()

# -----------------------------------------------------------------

def getFixedHeight(self):
if self.fSkinStyle == "tube":
return 98
return 80

# -----------------------------------------------------------------
@@ -1839,13 +1991,17 @@ class PluginSlot_Presets(AbstractPluginSlot):
self.peak_in = self.ui.peak_in
self.peak_out = self.ui.peak_out

if skinStyle == "zynfx":
# if skinStyle == "zynfx": # TODO jpka: TEST ing zynfx as normal tweakable skin
if False:
self.setupZynFxParams()
else:
self.w_knobs_left = self.ui.w_knobs_left
self.w_knobs_right = self.ui.w_knobs_right
self.spacer_knobs = self.ui.layout_bottom.itemAt(1).spacerItem()

if self.fTweaks.get('MoreSpace', 0):
self.ui.layout_bottom.setContentsMargins(0, 4, 0, 0)

self.ready()

if usingMidiPrograms:
@@ -1855,6 +2011,8 @@ class PluginSlot_Presets(AbstractPluginSlot):

# -------------------------------------------------------------

# it works only for internal zyn builds, which are disabled by default
# (?) not for just manual "zynfx" skin selection
def setupZynFxParams(self):
parameterCount = min(self.host.get_parameter_count(self.fPluginId), 8)

@@ -2001,6 +2159,46 @@ class PluginSlot_Presets(AbstractPluginSlot):

# ------------------------------------------------------------------------------------------------------------

def insertProgramList(self, layout, index):
count = self.host.get_program_count(self.fPluginId)
if count:
cb = QComboBox(None)
cb.setObjectName("cb_presets0") # use this stylesheet

for i in range(count):
string = self.host.get_program_name(self.fPluginId, i)

if len(string) == 0:
print("Carla: WARNING: Program List have zero length item.")
return

cb.addItem(string)

layout.insertWidget(index, cb)
cb.setCurrentIndex(self.host.get_current_program_index(self.fPluginId))
cb.currentIndexChanged.connect(self.slot_programChanged)

def insertMidiProgramList(self, layout, index):
count = self.host.get_midi_program_count(self.fPluginId)
if count:
cb = QComboBox(None)
cb.setObjectName("cb_presets1") # use this stylesheet

for i in range(count):
string = self.host.get_midi_program_data(self.fPluginId, i)['name']

if len(string) == 0:
print("Carla: WARNING: MIDI Program List have zero length item.")
return

cb.addItem(string)

layout.insertWidget(index, cb)
cb.setCurrentIndex(self.host.get_current_midi_program_index(self.fPluginId))
cb.currentIndexChanged.connect(self.slot_midiProgramChanged)

# ------------------------------------------------------------------------------------------------------------

def getColorAndSkinStyle(host, pluginId):
pluginInfo = host.get_plugin_info(pluginId)
pluginName = host.get_real_plugin_name(pluginId)
@@ -2018,9 +2216,9 @@ def getColorAndSkinStyle(host, pluginId):

# Samplers
if pluginInfo['type'] == PLUGIN_SF2:
return (colorCategory, "sf2")
return (colorCategory, "mpresets")
if pluginInfo['type'] == PLUGIN_SFZ:
return (colorCategory, "sfz")
return (colorCategory, "mpresets")

# Calf
if pluginName.split(" ", 1)[0].lower() == "calf":
@@ -2032,6 +2230,10 @@ def getColorAndSkinStyle(host, pluginId):
if pluginMaker == "OpenAV":
return (colorNone, "openav")

# Tube
if "tube" in pluginLabel:
return (colorCategory, "tube")

# ZynFX
if pluginInfo['type'] == PLUGIN_INTERNAL:
if pluginLabel.startswith("zyn") and pluginInfo['category'] != PLUGIN_CATEGORY_SYNTH:
@@ -2042,7 +2244,8 @@ def getColorAndSkinStyle(host, pluginId):
return (colorNone, "zynfx")

if pluginInfo['type'] == PLUGIN_LV2:
if pluginLabel.startswith("http://kxstudio.sf.net/carla/plugins/zyn") and pluginName != "ZynAddSubFX":
# if pluginLabel.startswith("http://kxstudio.sf.net/carla/plugins/zyn") and pluginName != "ZynAddSubFX":
if pluginLabel.startswith("http://kxstudio.sf.net/carla/plugins/zyn") and pluginName != "ZynAddSubFX" or "zyn" in pluginLabel: # jpka: TEST ing zynfx as normal tweakable skin
return (colorNone, "zynfx")

# Presets


+ 251
- 82
source/frontend/carla_widgets.py View File

@@ -6,6 +6,7 @@
# Imports (Global)

from abc import abstractmethod
import numpy as np

# ------------------------------------------------------------------------------------------------------------
# Imports (PyQt)
@@ -14,7 +15,7 @@ from qt_compat import qt_config

if qt_config == 5:
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray
from PyQt5.QtGui import QCursor, QIcon, QPalette, QPixmap
from PyQt5.QtGui import QCursor, QIcon, QPalette, QPixmap, QFont
from PyQt5.QtWidgets import (
QDialog,
QFileDialog,
@@ -24,10 +25,18 @@ if qt_config == 5:
QScrollArea,
QVBoxLayout,
QWidget,
QGraphicsScene,
QGraphicsTextItem,
QGraphicsView,
QTableWidget,
QTableWidgetItem,
QHeaderView,
QLabel,
QSizePolicy,
)
elif qt_config == 6:
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray
from PyQt6.QtGui import QCursor, QIcon, QPalette, QPixmap
from PyQt6.QtGui import QCursor, QIcon, QPalette, QPixmap, QFont
from PyQt6.QtWidgets import (
QDialog,
QFileDialog,
@@ -37,6 +46,14 @@ elif qt_config == 6:
QScrollArea,
QVBoxLayout,
QWidget,
QGraphicsScene,
QGraphicsTextItem,
QGraphicsView,
QTableWidget,
QTableWidgetItem,
QHeaderView,
QLabel,
QSizePolicy,
)

# ------------------------------------------------------------------------------------------------------------
@@ -60,6 +77,7 @@ from carla_backend import (
PLUGIN_CAN_VOLUME,
PLUGIN_CAN_BALANCE,
PLUGIN_CAN_PANNING,
PLUGIN_CAN_FORTH,
PLUGIN_CATEGORY_SYNTH,
PLUGIN_OPTION_FIXED_BUFFERS,
PLUGIN_OPTION_FORCE_STEREO,
@@ -72,11 +90,13 @@ from carla_backend import (
PLUGIN_OPTION_SEND_ALL_SOUND_OFF,
PLUGIN_OPTION_SEND_PROGRAM_CHANGES,
PLUGIN_OPTION_SKIP_SENDING_NOTES,
PARAMETER_NULL,
PARAMETER_DRYWET,
PARAMETER_VOLUME,
PARAMETER_BALANCE_LEFT,
PARAMETER_BALANCE_RIGHT,
PARAMETER_PANNING,
PARAMETER_FORTH,
PARAMETER_CTRL_CHANNEL,
PARAMETER_IS_ENABLED,
PARAMETER_IS_AUTOMATABLE,
@@ -84,6 +104,7 @@ from carla_backend import (
PARAMETER_USES_SCALEPOINTS,
PARAMETER_USES_CUSTOM_TEXT,
PARAMETER_CAN_BE_CV_CONTROLLED,
parameterHintsText,
PARAMETER_INPUT, PARAMETER_OUTPUT,
CONTROL_INDEX_NONE,
CONTROL_INDEX_MIDI_PITCHBEND,
@@ -94,6 +115,7 @@ from carla_backend import (
from carla_shared import (
MIDI_CC_LIST, MAX_MIDI_CC_LIST_ITEM,
countDecimalPoints,
strLim,
fontMetricsHorizontalAdvance,
setUpSignals,
gCarla
@@ -103,6 +125,8 @@ from carla_utils import getPluginTypeAsString

from widgets.collapsablewidget import CollapsibleBox
from widgets.pixmapkeyboard import PixmapKeyboardHArea
from widgets.paramspinbox import CustomInputDialog, ParamSpinBox
from widgets.scalabledial import ScalableDial

# ------------------------------------------------------------------------------------------------------------
# Carla GUI defines
@@ -119,6 +143,7 @@ class PluginParameter(QWidget):
mappedControlChanged = pyqtSignal(int, int)
mappedRangeChanged = pyqtSignal(int, float, float)
midiChannelChanged = pyqtSignal(int, int)
knobVisibilityChanged = pyqtSignal(int, int)
valueChanged = pyqtSignal(int, float)

def __init__(self, parent, host, pInfo, pluginId, tabIndex):
@@ -141,6 +166,7 @@ class PluginParameter(QWidget):
self.fParameterId = pInfo['index']
self.fPluginId = pluginId
self.fTabIndex = tabIndex
self.fKnobVisible = True

# -------------------------------------------------------------
# Set-up GUI
@@ -157,6 +183,7 @@ class PluginParameter(QWidget):
self.ui.widget.setStep(pInfo['step'])
self.ui.widget.setStepSmall(pInfo['stepSmall'])
self.ui.widget.setStepLarge(pInfo['stepLarge'])
# NOTE: Issue #1983
self.ui.widget.setScalePoints(pInfo['scalePoints'], bool(pHints & PARAMETER_USES_SCALEPOINTS))

if pInfo['comment']:
@@ -536,41 +563,26 @@ class PluginEdit(QDialog):
labelPluginFont.setWeight(75)
self.ui.label_plugin.setFont(labelPluginFont)

self.ui.dial_drywet.setCustomPaintMode(self.ui.dial_drywet.CUSTOM_PAINT_MODE_CARLA_WET)
self.ui.dial_drywet.setImage(3)
self.ui.dial_drywet.setLabel("Dry/Wet")
self.ui.dial_drywet.setMinimum(0.0)
self.ui.dial_drywet.setMaximum(1.0)
pluginHints = self.host.get_plugin_info(self.fPluginId)['hints']

self.ui.dial_drywet = ScalableDial(self.ui.dial_drywet, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET)
self.ui.dial_drywet.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_DRYWET))

self.ui.dial_vol.setCustomPaintMode(self.ui.dial_vol.CUSTOM_PAINT_MODE_CARLA_VOL)
self.ui.dial_vol.setImage(3)
self.ui.dial_vol.setLabel("Volume")
self.ui.dial_vol.setMinimum(0.0)
self.ui.dial_vol.setMaximum(1.27)
self.ui.dial_vol = ScalableDial(self.ui.dial_vol, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL)
self.ui.dial_vol.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_VOLUME))

self.ui.dial_b_left.setCustomPaintMode(self.ui.dial_b_left.CUSTOM_PAINT_MODE_CARLA_L)
self.ui.dial_b_left.setImage(4)
self.ui.dial_b_left.setLabel("L")
self.ui.dial_b_left.setMinimum(-1.0)
self.ui.dial_b_left.setMaximum(1.0)
self.ui.dial_b_left = ScalableDial(self.ui.dial_b_left, PARAMETER_BALANCE_LEFT, 100, -1.0, -1.0, 1.0, "L", ScalableDial.CUSTOM_PAINT_MODE_CARLA_L)
self.ui.dial_b_left.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_LEFT))

self.ui.dial_b_right.setCustomPaintMode(self.ui.dial_b_right.CUSTOM_PAINT_MODE_CARLA_R)
self.ui.dial_b_right.setImage(4)
self.ui.dial_b_right.setLabel("R")
self.ui.dial_b_right.setMinimum(-1.0)
self.ui.dial_b_right.setMaximum(1.0)
self.ui.dial_b_right = ScalableDial(self.ui.dial_b_right, PARAMETER_BALANCE_RIGHT, 100, 1.0, -1.0, 1.0, "R", ScalableDial.CUSTOM_PAINT_MODE_CARLA_R)
self.ui.dial_b_right.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_RIGHT))

self.ui.dial_pan.setCustomPaintMode(self.ui.dial_b_right.CUSTOM_PAINT_MODE_CARLA_PAN)
self.ui.dial_pan.setImage(4)
self.ui.dial_pan.setLabel("Pan")
self.ui.dial_pan.setMinimum(-1.0)
self.ui.dial_pan.setMaximum(1.0)
self.ui.dial_pan = ScalableDial(self.ui.dial_pan, PARAMETER_PANNING, 100, 0, -1.0, 1.0, "Pan", ScalableDial.CUSTOM_PAINT_MODE_CARLA_PAN)
self.ui.dial_pan.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_PANNING))

self.ui.dial_forth = ScalableDial(self.ui.dial_forth, PARAMETER_FORTH, 100, 0, -1.0, 1.0, "Forth", ScalableDial.CUSTOM_PAINT_MODE_CARLA_FORTH)
self.ui.dial_forth.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_FORTH))

self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)

self.ui.scrollArea = PixmapKeyboardHArea(self)
@@ -582,10 +594,10 @@ class PluginEdit(QDialog):
self.ui.scrollArea.setVisible(False)

# todo
self.ui.rb_balance.setEnabled(False)
self.ui.rb_balance.setVisible(False)
self.ui.rb_pan.setEnabled(False)
self.ui.rb_pan.setVisible(False)
# self.ui.rb_balance.setEnabled(False)
# self.ui.rb_balance.setVisible(False)
# self.ui.rb_pan.setEnabled(False)
# self.ui.rb_pan.setVisible(False)

flags = self.windowFlags()
flags &= ~Qt.WindowContextHelpButtonHint
@@ -617,6 +629,7 @@ class PluginEdit(QDialog):
self.ui.dial_b_left.realValueChanged.connect(self.slot_balanceLeftChanged)
self.ui.dial_b_right.realValueChanged.connect(self.slot_balanceRightChanged)
self.ui.dial_pan.realValueChanged.connect(self.slot_panChanged)
self.ui.dial_forth.realValueChanged.connect(self.slot_forthChanged)
self.ui.sb_ctrl_channel.valueChanged.connect(self.slot_ctrlChannelChanged)

self.ui.dial_drywet.customContextMenuRequested.connect(self.slot_knobCustomMenu)
@@ -751,6 +764,10 @@ class PluginEdit(QDialog):
self.ui.dial_pan.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_PANNING))
self.ui.dial_pan.blockSignals(False)

self.ui.dial_forth.blockSignals(True)
self.ui.dial_forth.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_FORTH))
self.ui.dial_forth.blockSignals(False)

self.fControlChannel = round(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_CTRL_CHANNEL))
self.ui.sb_ctrl_channel.blockSignals(True)
self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
@@ -820,6 +837,7 @@ class PluginEdit(QDialog):
self.ui.dial_b_left.setEnabled(pluginHints & PLUGIN_CAN_BALANCE)
self.ui.dial_b_right.setEnabled(pluginHints & PLUGIN_CAN_BALANCE)
self.ui.dial_pan.setEnabled(pluginHints & PLUGIN_CAN_PANNING)
self.ui.dial_forth.setEnabled(pluginHints & PLUGIN_CAN_FORTH)

optsAvailable = self.fPluginInfo['optionsAvailable']
optsEnabled = self.fPluginInfo['optionsEnabled']
@@ -901,7 +919,6 @@ class PluginEdit(QDialog):
break

paramData = self.host.get_parameter_data(self.fPluginId, i)

if paramData['type'] not in (PARAMETER_INPUT, PARAMETER_OUTPUT):
unusedParameters += 1
continue
@@ -974,6 +991,12 @@ class PluginEdit(QDialog):
self._createParameterWidgets(PARAMETER_INPUT, paramInputListFull, self.tr("Parameters"))
self._createParameterWidgets(PARAMETER_OUTPUT, paramOutputListFull, self.tr("Outputs"))

# Create full parameter list table tab
self._createParameterXrayTab(self.tr("XRay"))

# Create experimental description tab WORKINPROGRESS NOTE discussions/1967, pull/1961
self._createDescriptionTab(self.tr("Description"))

# Restore tab state
if tabIndex < self.ui.tabWidget.count():
self.ui.tabWidget.setCurrentIndex(tabIndex)
@@ -1197,6 +1220,11 @@ class PluginEdit(QDialog):
self.ui.dial_pan.setValue(value)
self.ui.dial_pan.blockSignals(False)

elif index == PARAMETER_FORTH:
self.ui.dial_forth.blockSignals(True)
self.ui.dial_forth.setValue(value)
self.ui.dial_forth.blockSignals(False)

elif index == PARAMETER_CTRL_CHANNEL:
self.fControlChannel = round(value)
self.ui.sb_ctrl_channel.blockSignals(True)
@@ -1396,6 +1424,13 @@ class PluginEdit(QDialog):
if self.fParent is not None:
self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_PANNING, value)

@pyqtSlot(float)
def slot_forthChanged(self, value):
self.host.set_forth(self.fPluginId, value)

if self.fParent is not None:
self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_FORTH, value)

@pyqtSlot(int)
def slot_ctrlChannelChanged(self, value):
self.fControlChannel = value-1
@@ -1474,69 +1509,67 @@ class PluginEdit(QDialog):

@pyqtSlot()
def slot_knobCustomMenu(self):
sender = self.sender()
knobName = sender.objectName()

if knobName == "dial_drywet":
minimum = 0.0
maximum = 1.0
default = 1.0
label = "Dry/Wet"
elif knobName == "dial_vol":
minimum = 0.0
maximum = 1.27
default = 1.0
label = "Volume"
elif knobName == "dial_b_left":
minimum = -1.0
maximum = 1.0
default = -1.0
label = "Balance-Left"
elif knobName == "dial_b_right":
minimum = -1.0
maximum = 1.0
default = 1.0
label = "Balance-Right"
elif knobName == "dial_pan":
minimum = -1.0
maximum = 1.0
default = 0.0
label = "Panning"
# jpka: NOTE now Edit knobs are also know their constraints, so it's worth to set values as normal ones.
sender = self.sender()
index = sender.fIndex
minimum = sender.fMinimum
maximum = sender.fMaximum
current = sender.fRealValue
label = sender.fLabel
default = sender.fDefault
unit = sender.fUnit
step = stepSmall = 1.0

if index < PARAMETER_NULL:
percent = 100.0
else:
minimum = 0.0
maximum = 1.0
default = 0.5
label = "Unknown"
percent = 1

textReset = self.tr("Reset (" + strLim(default * percent) + unit + ")\tR, Middle click")
textMinim = self.tr("Set to Minimum (" + strLim(minimum * percent) + unit + ")\t0")
textMaxim = self.tr("Set to Maximum (" + strLim(maximum * percent) + unit + ")\tEnd")

if sender.fIsButton:
editHotKey = "E"
else:
editHotKey = "Enter, Double click"

menu = QMenu(self)
actReset = menu.addAction(self.tr("Reset (%i%%)" % (default*100)))
actReset = menu.addAction(textReset)
menu.addSeparator()
actMinimum = menu.addAction(self.tr("Set to Minimum (%i%%)" % (minimum*100)))
actCenter = menu.addAction(self.tr("Set to Center"))
actMaximum = menu.addAction(self.tr("Set to Maximum (%i%%)" % (maximum*100)))
actMinimum = menu.addAction(textMinim)
actCenter = menu.addAction(self.tr("Set to Center\t5"))
actMaximum = menu.addAction(textMaxim)
menu.addSeparator()
actSet = menu.addAction(self.tr("Set value..."))
actSet = menu.addAction(self.tr("Set value...\t" + editHotKey))

if label not in ("Balance-Left", "Balance-Right", "Panning"):
if index > PARAMETER_NULL or index not in (PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING, PARAMETER_FORTH):
menu.removeAction(actCenter)

actSelected = menu.exec_(QCursor.pos())

if actSelected == actSet:
current = minimum + (maximum-minimum)*(float(sender.value())/10000)
value, ok = QInputDialog.getInt(self,
self.tr("Set value"),
label,
round(current*100.0),
round(minimum*100.0),
round(maximum*100.0),
1)
if ok:
value = float(value)/100.0
paramInfo = self.host.get_parameter_info(self.fPluginId, index)
paramRanges = self.host.get_parameter_ranges(self.fPluginId, index)
scalePoints = []

if not ok:
for i in range(paramInfo['scalePointCount']):
scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, index, i))

if sender.fIsInteger:
step = max(1, int((maximum - minimum)/100))
stepSmall = max(1, int(step/10))
else:
step = paramRanges['step'] * percent
stepSmall = paramRanges['stepSmall'] * percent

dialog = CustomInputDialog(self, label, current * percent, minimum * percent, maximum * percent, step, stepSmall, scalePoints, "", "", unit)

if not dialog.exec_():
return

value = dialog.returnValue() / percent

elif actSelected == actMinimum:
value = minimum
elif actSelected == actMaximum:
@@ -1605,13 +1638,15 @@ class PluginEdit(QDialog):
scrollAreaLayout = QVBoxLayout(scrollAreaWidget)
scrollAreaLayout.setSpacing(3)

expandBox = (len(paramList) < 50)

for paramInfo in paramList:
groupName = paramInfo['groupName']
if groupName:
groupSymbol, groupName = groupName.split(":",1)
groupLayout, groupWidget = groupWidgets.get(groupSymbol, (None, None))
if groupLayout is None:
groupWidget = CollapsibleBox(groupName, scrollAreaWidget)
groupWidget = CollapsibleBox(groupName, scrollAreaWidget, expandBox)
groupLayout = groupWidget.getContentLayout()
groupWidget.setPalette(palette2)
scrollAreaLayout.addWidget(groupWidget)
@@ -1678,6 +1713,140 @@ class PluginEdit(QDialog):

#------------------------------------------------------------------

# NOTE To speed things up, displayed data is not realtime. Reopen project to expose last changes.
def _createParameterXrayTab(self, tabName):
# How simple would be fit a value into cell? Yet to be as fast as we can.
def strFit(value):
if isinstance(value, str):
return value
# For 'carla-control', but anyway scalePoints are not work. #1984
elif isinstance(value, list):
return str(value) # It's []
elif abs(value) >= 1E8:
return '{:.3e}'.format(value)
elif value == int(value): # Zero falls here
return str(int(value))
else:
return strLim(value)

def strLineWrap(string, cut):
result = ''
while len(string) > cut: # FIXME Optimize me!
result += string[:cut] + '\n'
string = string[cut:]
result += string
return result

def addCell(section, name, string, toolTip = ''):
if x == table.columnCount():
table.insertColumn(x)
# jpka: FIXME Here we need vertical text. But impossible, no working examples.
# Only untested https://stackoverflow.com/questions/52162125/
nameWrapped = section + '\n' + strLineWrap(name, 6)
table.setHorizontalHeaderItem(x, QTableWidgetItem(nameWrapped))
table.horizontalHeader().setSectionResizeMode(x, QHeaderView.ResizeToContents)

if y == table.rowCount():
table.insertRow(y)
table.verticalHeader().setSectionResizeMode(y, QHeaderView.ResizeToContents)

item = QTableWidgetItem(string)
if toolTip:
item.setToolTip(toolTip)
table.setItem(y, x, item)
return

table = QTableWidget(self)
table.setObjectName("table")
table.setRowCount(1)
# table.setToolTipDuration(2000)

parameterCount = self.host.get_parameter_count(self.fPluginId)
if parameterCount <= 0:
return

y = 0
for i in range(parameterCount):
x = 0
param = self.host.get_parameter_data(self.fPluginId, i)
for name in param:
value = param[name]
if (name == 'type') and (value in (1, 2,)):
addCell('Data', name, str(value) + (' in',' out')[value - 1])
elif (name == 'hints'):
# toolTip = '<body>' + bin(value)[2:]
hints = ''
for bit in range(len(parameterHintsText)):
if (value & int(2**(bit-1))):
hint = parameterHintsText[bit-1]
# toolTip += '<br>' + hint
hints += ', ' + hint
addCell('Data', name, str(value)) # , toolTip + '</body>')
x += 1
addCell('Hints', '', hints[2:])
else:
addCell('Data', name, strFit(value))
x += 1

param = self.host.get_parameter_ranges(self.fPluginId, i)
for name in param:
addCell('Ranges', name, strFit(param[name]))
x += 1

param = self.host.get_parameter_info(self.fPluginId, i)
for name in param:
addCell('Info', name, strFit(param[name]))
x += 1

strScalePoints = ''
for j in range(param['scalePointCount']):
scalePointInfo = self.host.get_parameter_scalepoint_info(self.fPluginId, i, j)
strScalePoints += (strFit(scalePointInfo['value']) + ':' + scalePointInfo['label'] + ',')

if strScalePoints:
addCell('Scalepoint_info', 'Scale Points', strLineWrap(strScalePoints[:len(strScalePoints)-1], 80))
x += 1

y += 1

self.ui.tabWidget.addTab(table, tabName)
# self.ui.tabWidget.setToolTipDuration(2000)

#------------------------------------------------------------------

def _createDescriptionTab(self, tabPageName):
# jpka: To be filled from 'rdfs:comment'
strDescr = "To be filled from rdfs:comment"

realPluginName = self.host.get_real_plugin_name(self.fPluginId)
labelURI = self.fPluginInfo['label']

strLoadState = ""
programCount = self.host.get_program_count(self.fPluginId)
if programCount > 0:
strLoadState = '<div style="letter-spacing:1px"><br>'\
'<b>Note: </b>This plugin collected some presets for you.<br>'\
'Use <i>Edit</i> tab, then <i>Load State</i> button.</div>'

scene = QGraphicsScene(self)
text = QGraphicsTextItem("",None)
text.setTextInteractionFlags(Qt.TextSelectableByMouse)
text.setTextWidth(600);
# text.setFont(QFont("Arial, 16")) # NOTE: All Qt sizes are in Pt; real px~=4/3Pt.
text.setHtml('<body>\
<h1>' + realPluginName + '</h1><br>'\
'<a href=' + labelURI + '>' + labelURI + '</a><br><br>'\
'<div style="line-height:1.5;">' + strDescr + '</div>' +\
strLoadState +\
'<body>');

scene.addItem(text)
view = QGraphicsView(scene, self)

self.ui.tabWidget.addTab(view, tabPageName)

#------------------------------------------------------------------

def testTimer(self):
self.fIdleTimerId = self.startTimer(50)



+ 1
- 0
source/frontend/dialogs/aboutdialog.cpp View File

@@ -61,6 +61,7 @@ AboutDialog::AboutDialog(QWidget* const parent,
"<tr><td>" "/set_volume" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
"<tr><td>" "/set_balance_left" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
"<tr><td>" "/set_balance_right" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
"<tr><td>" "/set_forth" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
"<tr><td>" "/set_panning" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
"<tr><td>" "/set_parameter_value" "&nbsp;</td><td>&lt;i-index&gt; &lt;f-value&gt;</td></tr>"
"<tr><td>" "/set_parameter_midi_cc" "&nbsp;</td><td>&lt;i-index&gt; &lt;i-cc&gt;</td></tr>"


+ 20
- 0
source/frontend/qt_compat.py View File

@@ -32,6 +32,8 @@ elif qt_config == 6:
QMessageBox,
QSizePolicy,
QStyle,
QToolButton,
QToolBar,
)

Qt.AlignCenter = Qt.AlignmentFlag.AlignCenter
@@ -60,9 +62,16 @@ elif qt_config == 6:
Qt.CrossCursor = Qt.CursorShape.CrossCursor
Qt.OpenHandCursor = Qt.CursorShape.OpenHandCursor
Qt.PointingHandCursor = Qt.CursorShape.PointingHandCursor
Qt.RightArrow = Qt.ArrowType.RightArrow
Qt.DownArrow = Qt.ArrowType.DownArrow
Qt.SizeAllCursor = Qt.CursorShape.SizeAllCursor
Qt.SizeHorCursor = Qt.CursorShape.SizeHorCursor

Qt.TextSelectableByMouse = Qt.TextInteractionFlag.TextSelectableByMouse

Qt.ToolButtonIconOnly = Qt.ToolButtonStyle.ToolButtonIconOnly
Qt.ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon

Qt.black = Qt.GlobalColor.black
Qt.blue = Qt.GlobalColor.blue
Qt.cyan = Qt.GlobalColor.cyan
@@ -116,6 +125,13 @@ elif qt_config == 6:
Qt.Key_X = Qt.Key.Key_X
Qt.Key_Y = Qt.Key.Key_Y
Qt.Key_Z = Qt.Key.Key_Z
Qt.Key_Space = Qt.Key.Key_Space
Qt.Key_Enter = Qt.Key.Key_Enter
Qt.Key_Return = Qt.Key.Key_Return
Qt.Key_PageUp = Qt.Key.Key_PageUp
Qt.Key_PageDown = Qt.Key.Key_PageDown
Qt.Key_Home = Qt.Key.Key_Home
Qt.Key_End = Qt.Key.Key_End

Qt.AltModifier = Qt.KeyboardModifier.AltModifier
Qt.ControlModifier = Qt.KeyboardModifier.ControlModifier
@@ -140,8 +156,10 @@ elif qt_config == 6:
Qt.Horizontal = Qt.Orientation.Horizontal

Qt.FlatCap = Qt.PenCapStyle.FlatCap
Qt.RoundCap = Qt.PenCapStyle.RoundCap

Qt.MiterJoin = Qt.PenJoinStyle.MiterJoin
Qt.RoundJoin = Qt.PenJoinStyle.RoundJoin

Qt.DashLine = Qt.PenStyle.DashLine
Qt.NoPen = Qt.PenStyle.NoPen
@@ -181,6 +199,7 @@ elif qt_config == 6:

QDialog.exec_ = lambda d: d.exec()

QDialogButtonBox.Ok = QDialogButtonBox.StandardButton.Ok
QDialogButtonBox.Reset = QDialogButtonBox.StandardButton.Reset

QEvent.EnabledChange = QEvent.Type.EnabledChange
@@ -225,6 +244,7 @@ elif qt_config == 6:
QGraphicsView.MinimalViewportUpdate = QGraphicsView.ViewportUpdateMode.MinimalViewportUpdate

QHeaderView.Fixed = QHeaderView.ResizeMode.Fixed
QHeaderView.ResizeToContents = QHeaderView.ResizeMode.ResizeToContents

QLineEdit.Normal = QLineEdit.EchoMode.Normal



+ 5
- 3
source/frontend/widgets/collapsablewidget.py View File

@@ -33,7 +33,7 @@ class QToolButtonWithMouseTracking(QToolButton):
QToolButton.leaveEvent(self, event)

class CollapsibleBox(QFrame):
def __init__(self, title, parent):
def __init__(self, title, parent, startsExpanded = True):
QFrame.__init__(self, parent)

self.setFrameShape(QFrame.StyledPanel)
@@ -42,10 +42,10 @@ class CollapsibleBox(QFrame):
self.toggle_button = QToolButtonWithMouseTracking(self)
self.toggle_button.setText(title)
self.toggle_button.setCheckable(True)
self.toggle_button.setChecked(True)
self.toggle_button.setChecked(startsExpanded)
self.toggle_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(Qt.DownArrow)
# self.toggle_button.setArrowType(Qt.DownArrow) # Not deleted, just moved
self.toggle_button.toggled.connect(self.toolButtonPressed)

self.content_area = QWidget(self)
@@ -59,6 +59,8 @@ class CollapsibleBox(QFrame):
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)

self.toolButtonPressed(startsExpanded) # Set initial state

@pyqtSlot(bool)
def toolButtonPressed(self, toggled):
self.content_area.setVisible(toggled)


+ 408
- 95
source/frontend/widgets/commondial.py View File

@@ -5,18 +5,23 @@
# ---------------------------------------------------------------------------------------------------------------------
# Imports (Global)

from math import isnan
from math import isnan, log10
import numpy as np

from qt_compat import qt_config

if qt_config == 5:
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QRectF, QEvent, QTimer
from PyQt5.QtGui import QColor, QFont, QLinearGradient, QPainter
from PyQt5.QtWidgets import QDial
from PyQt5.QtWidgets import QWidget, QToolTip, QInputDialog
elif qt_config == 6:
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QRectF, QEvent, QTimer
from PyQt6.QtGui import QColor, QFont, QLinearGradient, QPainter
from PyQt6.QtWidgets import QDial
from PyQt6.QtWidgets import QWidget, QToolTip, QInputDialog

from carla_shared import strLim
from widgets.paramspinbox import CustomInputDialog
from carla_backend import PARAMETER_NULL, PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING, PARAMETER_FORTH

# ---------------------------------------------------------------------------------------------------------------------
# Widget Class
@@ -25,7 +30,7 @@ elif qt_config == 6:
#def updateSizes(self):
#def paintDial(self, painter):

class CommonDial(QDial):
class CommonDial(QWidget):
# enum CustomPaintMode
CUSTOM_PAINT_MODE_NULL = 0 # default (NOTE: only this mode has label gradient)
CUSTOM_PAINT_MODE_CARLA_WET = 1 # color blue-green gradient (reserved #3)
@@ -33,9 +38,11 @@ class CommonDial(QDial):
CUSTOM_PAINT_MODE_CARLA_L = 3 # color yellow (reserved #4)
CUSTOM_PAINT_MODE_CARLA_R = 4 # color yellow (reserved #4)
CUSTOM_PAINT_MODE_CARLA_PAN = 5 # color yellow (reserved #3)
CUSTOM_PAINT_MODE_COLOR = 6 # color, selectable (reserved #3)
CUSTOM_PAINT_MODE_ZITA = 7 # custom zita knob (reserved #6)
CUSTOM_PAINT_MODE_CARLA_FORTH = 6 # Experimental
CUSTOM_PAINT_MODE_COLOR = 7 # May be deprecated (unless zynfx internal mode)
CUSTOM_PAINT_MODE_NO_GRADIENT = 8 # skip label gradient
CUSTOM_PAINT_MODE_CARLA_WET_MINI = 9 # for compacted slot
CUSTOM_PAINT_MODE_CARLA_VOL_MINI = 10 # for compacted slot

# enum Orientation
HORIZONTAL = 0
@@ -51,16 +58,37 @@ class CommonDial(QDial):
dragStateChanged = pyqtSignal(bool)
realValueChanged = pyqtSignal(float)

def __init__(self, parent, index):
QDial.__init__(self, parent)
def __init__(self, parent, index, precision, default, minimum, maximum, label, paintMode, colorHint, unit, skinStyle, whiteLabels, tweaks, isInteger, isButton, isOutput, isVuOutput, isVisible):
QWidget.__init__(self, parent)

self.fIndex = index
self.fPrecision = precision
self.fDefault = default
self.fMinimum = minimum
self.fMaximum = maximum
self.fCustomPaintMode = paintMode
self.fColorHint = colorHint
self.fUnit = unit
self.fSkinStyle = skinStyle
self.fWhiteLabels = whiteLabels
self.fTweaks = tweaks
self.fIsInteger = isInteger
self.fIsButton = isButton
self.fIsOutput = isOutput
self.fIsVuOutput = isVuOutput
self.fIsVisible = isVisible

self.fDialMode = self.MODE_LINEAR

self.fMinimum = 0.0
self.fMaximum = 1.0
self.fLabel = label
self.fLastLabel = ""
self.fRealValue = 0.0
self.fPrecision = 10000
self.fIsInteger = False
self.fLastValue = self.fDefault

self.fScalePoints = []
self.fNumScalePoints = 0
self.fScalePointsPrefix = ""
self.fScalePointsSuffix = ""

self.fIsHovered = False
self.fIsPressed = False
@@ -69,9 +97,6 @@ class CommonDial(QDial):
self.fLastDragPos = None
self.fLastDragValue = 0.0

self.fIndex = index

self.fLabel = ""
self.fLabelPos = QPointF(0.0, 0.0)
self.fLabelFont = QFont(self.font())
self.fLabelFont.setPixelSize(8)
@@ -97,75 +122,79 @@ class CommonDial(QDial):

self.fLabelGradientRect = QRectF(0.0, 0.0, 0.0, 0.0)

self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_NULL
self.fCustomPaintColor = QColor(0xff, 0xff, 0xff)

# Fake internal value, custom precision
QDial.setMinimum(self, 0)
QDial.setMaximum(self, self.fPrecision)
QDial.setValue(self, 0)
self.addContrast = int(bool(self.getTweak('HighContrast', 0)))
self.colorFollow = bool(self.getTweak('ColorFollow', 0))
self.knobPusheable = bool(self.getTweak('WetVolPush', 0))
self.displayTooltip = bool(self.getTweak('Tooltips', 1))

self.valueChanged.connect(self.slot_valueChanged)
# We have two group of knobs, non-repaintable (like in Edit dialog) and normal.
# For non-repaintable, we init sizes/color once here;
# for normals, it should be (re)inited separately: we do not init it here
# to save CPU, some parameters are not known yet, repaint need anyway.
if self.fColorHint == -1:
self.updateSizes()

self.update()

# self.valueChanged.connect(self.slot_valueChanged) # FIXME

def forceWhiteLabelGradientText(self):
self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
self.fLabelGradientColorT = [Qt.white, Qt.darkGray]

def setLabelColor(self, enabled, disabled):
self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
self.fLabelGradientColorT = [enabled, disabled]
# def setLabelColor(self, enabled, disabled):
# self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
# self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
# self.fLabelGradientColorT = [enabled, disabled]

def getIndex(self):
return self.fIndex

def setIndex(self, index):
self.fIndex = index

def setPrecision(self, value, isInteger):
self.fPrecision = value
self.fIsInteger = isInteger
QDial.setMaximum(self, int(value))

def setMinimum(self, value):
self.fMinimum = value

def setMaximum(self, value):
self.fMaximum = value

def rvalue(self):
return self.fRealValue

def pushLabel(self, label):
if self.fLastLabel == "":
self.fLastLabel = self.fLabel
self.fLabel = label
self.updateSizes()
self.update()

def popLabel(self):
if not (self.fLastLabel == ""):
self.fLabel = self.fLastLabel
self.fLastLabel = ""
self.updateSizes()
self.update()

def setScalePPS(self, scalePoints, prefix, suffix):
self.fScalePoints = scalePoints
self.fNumScalePoints = len(self.fScalePoints)
self.fScalePointsPrefix = prefix
self.fScalePointsSuffix = suffix

def setValue(self, value, emitSignal=False):
if self.fRealValue == value or isnan(value):
return

if value <= self.fMinimum:
qtValue = 0
if (not self.fIsOutput) and value <= self.fMinimum:
self.fRealValue = self.fMinimum

elif value >= self.fMaximum:
qtValue = int(self.fPrecision)
elif (not self.fIsOutput) and value >= self.fMaximum:
self.fRealValue = self.fMaximum

elif self.fIsInteger or (abs(value - int(value)) < 1e-8): # tiny "notch"
self.fRealValue = round(value)

else:
qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision)
self.fRealValue = value

# Block change signal, we'll handle it ourselves
self.blockSignals(True)
QDial.setValue(self, qtValue)
self.blockSignals(False)

if emitSignal:
self.realValueChanged.emit(self.fRealValue)

def setCustomPaintMode(self, paintMode):
if self.fCustomPaintMode == paintMode:
return

self.fCustomPaintMode = paintMode
self.update()

def setCustomPaintColor(self, color):
@@ -173,76 +202,262 @@ class CommonDial(QDial):
return

self.fCustomPaintColor = color
self.updateSizes()
self.update()

def setLabel(self, label):
if self.fLabel == label:
return
def getTweak(self, tweakName, default):
return self.fTweaks.get(self.fSkinStyle + tweakName, self.fTweaks.get(tweakName, default))

self.fLabel = label
self.updateSizes()
self.update()
def getIsVisible(self):
# print (self.fIsVisible)
return self.fIsVisible

@pyqtSlot(int)
def slot_valueChanged(self, value):
self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum
self.realValueChanged.emit(self.fRealValue)

# jpka: TODO should be replaced by common dialog, but
# PluginEdit.slot_knobCustomMenu(...) - not found, import not work.
# So this is copy w/o access to 'step's.
def knobCustomInputDialog(self):
if self.fIndex < PARAMETER_NULL:
percent = 100.0
else:
percent = 1

if self.fIsInteger:
step = max(1, int((self.fMaximum - self.fMinimum)/100))
stepSmall = max(1, int(step/10))
else:
step = 10 ** (round(log10((self.fMaximum - self.fMinimum) * percent))-2)
stepSmall = step / 100

dialog = CustomInputDialog(self, self.fLabel, self.fRealValue * percent, self.fMinimum * percent, self.fMaximum * percent, step, stepSmall, self.fScalePoints, "", "", self.fUnit)
if not dialog.exec_():
return

self.setValue(dialog.returnValue() / percent, True)


def enterEvent(self, event):
self.setFocus()
self.fIsHovered = True
if self.fHoverStep == self.HOVER_MIN:
self.fHoverStep = self.HOVER_MIN + 1
QDial.enterEvent(self, event)
self.update()


def leaveEvent(self, event):
self.fIsHovered = False
if self.fHoverStep == self.HOVER_MAX:
self.fHoverStep = self.HOVER_MAX - 1
QDial.leaveEvent(self, event)
self.update()


def nextScalePoint(self):
for i in range(self.fNumScalePoints):
value = self.fScalePoints[i]['value']
if value > self.fRealValue:
self.setValue(value, True)
return
self.setValue(self.fScalePoints[0]['value'], True)


def mousePressEvent(self, event):
if self.fDialMode == self.MODE_DEFAULT:
QDial.mousePressEvent(self, event)
if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput:
return

if event.button() == Qt.LeftButton:
self.fIsPressed = True
self.fLastDragPos = event.pos()
self.fLastDragValue = self.fRealValue
self.dragStateChanged.emit(True)
# if self.fNumScalePoints:
# self.nextScalePoint()
#
if self.fIsButton:
value = int(self.fRealValue) + 1;
if (value > self.fMaximum):
value = 0
self.setValue(value, True)
else:
self.fIsPressed = True
self.fLastDragPos = event.pos()
self.fLastDragValue = self.fRealValue
self.dragStateChanged.emit(True)

elif event.button() == Qt.MiddleButton:
if self.fIsOutput:
return
self.setValue(self.fDefault, True)


def mouseDoubleClickEvent(self, event):
if self.knobPusheable and self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING, PARAMETER_FORTH): # -3, -4, -7, -9
return # Mutex with special Single Click

if event.button() == Qt.LeftButton:
if self.fIsButton:
value = int(self.fRealValue) + 1;
if (value > self.fMaximum):
value = 0
self.setValue(value, True)
else:
if self.fIsOutput:
return

self.knobCustomInputDialog()


def mouseMoveEvent(self, event):
if self.fDialMode == self.MODE_DEFAULT:
QDial.mouseMoveEvent(self, event)
if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput:
return

if not self.fIsPressed:
return

diff = (self.fMaximum - self.fMinimum) / 4.0
pos = event.pos()
dx = diff * float(pos.x() - self.fLastDragPos.x()) / self.width()
dy = diff * float(pos.y() - self.fLastDragPos.y()) / self.height()
value = self.fLastDragValue + dx - dy
pos = event.pos()
delta = (float(pos.x() - self.fLastDragPos.x()) - float(pos.y() - self.fLastDragPos.y())) / 10

if value < self.fMinimum:
value = self.fMinimum
elif value > self.fMaximum:
value = self.fMaximum
elif self.fIsInteger:
value = float(round(value))
mod = event.modifiers()
self.applyDelta(mod, delta, True)

self.setValue(value, True)

def mouseReleaseEvent(self, event):
if self.fDialMode == self.MODE_DEFAULT:
QDial.mouseReleaseEvent(self, event)
if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput:
return

if self.fIsPressed:
self.fIsPressed = False
self.dragStateChanged.emit(False)

if event.button() == Qt.LeftButton:
if event.pos() == self.fLastDragPos:
if self.fNumScalePoints:
self.nextScalePoint()
else:
self.knobPush()

# NOTE: fLastLabel state managed @ scalabledial
def knobPush(self):
if self.knobPusheable and self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING, PARAMETER_FORTH): # -3, -4, -7, -9

if self.fLastLabel == "": # push value
self.fLastValue = self.fRealValue
self.setValue(0, True) # Thru or Mute
else: # pop value
self.setValue(self.fLastValue, True)


def applyDelta(self, mod, delta, anchor = False):
if self.fIsOutput:
return

if self.fIsButton:
self.setValue(self.fRealValue + delta, True)
return

if self.fIsInteger: # 4 to 50 ticks per revolution
if (mod & Qt.ShiftModifier):
delta = delta * 5
elif (mod & Qt.ControlModifier):
delta = delta / min(int((self.fMaximum-self.fMinimum)/self.fPrecision), 5)
else: # Floats are 250 to 500 ticks per revolution
# jpka: 1. Should i use these steps?
# 2. And what do i do when i TODO add MODE_LOG along with MODE_LINEAR?
# 3. And they're too small for large ints like in TAP Reverb, and strange for scalepoints.
# paramRanges = self.host.get_parameter_ranges(self.fPluginId, i)
# paramRanges['step'], paramRanges['stepSmall'], paramRanges['stepLarge']
if (mod & Qt.ControlModifier) and (mod & Qt.ShiftModifier):
delta = delta * 2/5
elif (mod & Qt.ControlModifier):
delta = delta * 2
elif (mod & Qt.ShiftModifier):
delta = delta * 50
else:
delta = delta * 10

difference = float(self.fMaximum-self.fMinimum) * float(delta) / float(self.fPrecision)

if anchor:
self.setValue((self.fLastDragValue + difference), True)
else:
self.setValue((self.fRealValue + difference), True)

return


def wheelEvent(self, event):
if self.fIsOutput:
return

direction = event.angleDelta().y()
if direction < 0:
delta = -1.0
elif direction > 0:
delta = 1.0
else:
return

mod = event.modifiers()
self.applyDelta(mod, delta)
return


def keyPressEvent(self, event):
if self.fIsOutput:
return

key = event.key()
mod = event.modifiers()
modsNone = not ((mod & Qt.ShiftModifier) | (mod & Qt.ControlModifier) | (mod & Qt.AltModifier))

if modsNone:
match key:
case Qt.Key_Space | Qt.Key_Enter | Qt.Key_Return :
if self.fIsButton:
value = int(self.fRealValue) + 1
if (value > self.fMaximum):
value = 0
self.setValue(value, True)

elif not key == Qt.Key_Space:
self.knobCustomInputDialog()
else:
if self.fNumScalePoints:
self.nextScalePoint()
else:
self.knobPush()

case Qt.Key_E:
self.knobCustomInputDialog()

case key if Qt.Key_0 <= key <= Qt.Key_9:
if self.fIsInteger and (self.fMinimum == 0) and (self.fMaximum <= 10):
self.setValue(key-Qt.Key_0, True)

else:
self.setValue(self.fMinimum + float(self.fMaximum-self.fMinimum)/10.0*(key-Qt.Key_0), True)

case Qt.Key_Home: # NOTE: interferes with Canvas control hotkey
self.setValue(self.fMinimum, True)

case Qt.Key_End:
self.setValue(self.fMaximum, True)

case Qt.Key_D:
self.setValue(self.fDefault, True)

case Qt.Key_R:
self.setValue(self.fDefault, True)

match key:
case Qt.Key_PageDown:
self.applyDelta(mod, -1)

case Qt.Key_PageUp:
self.applyDelta(mod, 1)

return


def paintEvent(self, event):
painter = QPainter(self)
event.accept()
@@ -250,22 +465,120 @@ class CommonDial(QDial):
painter.save()
painter.setRenderHint(QPainter.Antialiasing, True)

enabled = int(bool(self.isEnabled()))
if enabled:
self.setContextMenuPolicy(Qt.CustomContextMenu)
else:
self.setContextMenuPolicy(Qt.NoContextMenu)

if self.fLabel:
if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
painter.setPen(self.fLabelGradientColor2)
painter.setBrush(self.fLabelGradient)
painter.drawRect(self.fLabelGradientRect)
# if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
# painter.setPen(self.fLabelGradientColor2)
# painter.setBrush(self.fLabelGradient)
# # painter.drawRect(self.fLabelGradientRect) FIXME restore gradients.

luma = int(bool(self.fWhiteLabels)) - 0.5
if enabled:
L = (luma * (1.6 + self.addContrast * 0.4)) / 2 + 0.5
else:
L = (luma * (0.2 + self.addContrast * 0.2)) / 2 + 0.5

painter.setFont(self.fLabelFont)
painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1])
# painter.setPen(self.fLabelGradientColorT[0 if self.fIsEnabled() else 1])
painter.setPen(QColor.fromHslF(0, 0, L, 1))
painter.drawText(self.fLabelPos, self.fLabel)

self.paintDial(painter)
X = self.fWidth / 2
Y = self.fHeight / 2

S = enabled * 0.9 # saturation

E = enabled * self.fHoverStep / 40 # enlight
L = 0.6 + E
if self.addContrast:
L = min(L + 0.3, 1) # luma

normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum)
# Work In Progress FIXME
H=0
if self.fIsOutput:
if self.fIsButton:
# self.paintLed (painter, X, Y, H, S, L, E, normValue)
self.paintDisplay(painter, X, Y, H, S, L, E, normValue, enabled)
else:
self.paintDisplay(painter, X, Y, H, S, L, E, normValue, enabled)
else:
if self.fIsButton:
self.paintButton (painter, X, Y, H, S, L, E, normValue, enabled)
else:
self.paintDial (painter, X, Y, H, S, L, E, normValue, enabled)

# Display tooltip, above the knob (OS-independent, unlike of mouse tooltip).
# Note, update/redraw Qt's tooltip eats much more CPU than expected,
# so we have tweak for turn it off. See also #1934.
if self.fHoverStep == self.HOVER_MAX and self.displayTooltip:
# First, we need to find exact or nearest match (index from value).
# It is also tests if we have scale points at all.
num = -1
for i in range(self.fNumScalePoints):
scaleValue = self.fScalePoints[i]['value']
if i == 0:
finalValue = scaleValue
num = 0
else:
srange2 = abs(self.fRealValue - finalValue)
srange1 = abs(self.fRealValue - scaleValue)
if srange2 > srange1:
finalValue = scaleValue
num = i
if (srange1 == 0): # Exact match, save some CPU.
break

tip = ""
if (num >= 0): # Scalepoints are used
tip = str(self.fScalePoints[num]['label'])
if not self.fIsButton:
tip = self.fScalePointsPrefix + \
strLim(self.fScalePoints[num]['value']) + \
self.fScalePointsSuffix + ": " + tip
# ? We most probably not need tooltip for button, if it is not scalepoint.
# elif not self.fIsButton:
else:
if self.fRealValue == 0 and self.fIndex == PARAMETER_DRYWET: #-3,-4,-7,-9
tip = "THRU"
elif self.fRealValue == 0 and self.fIndex == PARAMETER_VOLUME:
tip = "MUTE"
elif self.fRealValue == 0 and self.fIndex == PARAMETER_PANNING:
tip = "Center"
elif self.fRealValue == 0 and self.fIndex == PARAMETER_FORTH:
tip = "Midway"
else:
if self.fIndex < PARAMETER_NULL:
percent = 100.0
else:
percent = 1

tip = (strLim(self.fRealValue * percent) + " " + self.fUnit).strip()
if self.fIsOutput:
tip = tip + " [" + strLim(self.fMinimum * percent) + "..." + \
strLim(self.fMaximum * percent) + "]"

# Wrong vert. position for Calf:
# QToolTip.showText(self.mapToGlobal(QPoint(0, 0-self.geometry().height())), tip)
# FIXME Still wrong vert. position for QT_SCALE_FACTOR=2.
QToolTip.showText(self.mapToGlobal(QPoint(0, 0-45)), tip)
else:
QToolTip.hideText()

if enabled:
if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX:
self.fHoverStep += 1 if self.fIsHovered else -1
QTimer.singleShot(20, self.update)

painter.restore()

def resizeEvent(self, event):
QDial.resizeEvent(self, event)
self.updateSizes()
# def resizeEvent(self, event):
# QWidget.resizeEvent(self, event)
# self.updateSizes()

# ---------------------------------------------------------------------------------------------------------------------

+ 37
- 8
source/frontend/widgets/digitalpeakmeter.py View File

@@ -35,6 +35,7 @@ class DigitalPeakMeter(QWidget):
STYLE_OPENAV = 2
STYLE_RNCBC = 3
STYLE_CALF = 4
STYLE_TUBE = 5

# -----------------------------------------------------------------------------------------------------------------

@@ -153,7 +154,7 @@ class DigitalPeakMeter(QWidget):
if self.fMeterStyle == style:
return

if style not in (self.STYLE_DEFAULT, self.STYLE_OPENAV, self.STYLE_RNCBC, self.STYLE_CALF):
if style not in (self.STYLE_DEFAULT, self.STYLE_OPENAV, self.STYLE_RNCBC, self.STYLE_CALF, self.STYLE_TUBE):
qCritical(f"DigitalPeakMeter::setMeterStyle({style}) - invalid style")
return

@@ -163,7 +164,7 @@ class DigitalPeakMeter(QWidget):
self.fMeterBackground = QColor("#1A1A1A")
elif style == self.STYLE_RNCBC:
self.fMeterBackground = QColor("#070707")
elif style == self.STYLE_CALF:
elif style in (self.STYLE_CALF, self.STYLE_TUBE):
self.fMeterBackground = QColor("#000")

if style == self.STYLE_CALF:
@@ -215,23 +216,36 @@ class DigitalPeakMeter(QWidget):

i = meter - 1

if level < 0.001:
level = 0.0
elif level > 0.999:
level = 1.0

if self.fSmoothMultiplier > 0 and not forced:
level = (
(self.fLastChannelData[i] * float(self.fSmoothMultiplier) + level)
/ float(self.fSmoothMultiplier + 1)
)

if level < 0.001:
level = 0.0
elif level > 0.999:
level = 1.0
self.fLastChannelData[i] = level

# Discretize scale: for 10 points, first will lit at 5%,
# then 15%, and last at 95% of normalized value.
# We also win some CPU when not redraw at small changes.
if (self.fMeterStyle == self.STYLE_TUBE) and (level > 0.0) and (level < 1.0):
points = 20

# Transform to Sq Root domain: our meters have Sqrt dynamic compression;
# Discretize:
level = int(sqrt(level) * points + 0.5) / points

# Transform back from Square Root domain:
level = level * level

if self.fChannelData[i] != level:
self.fChannelData[i] = level
self.update()

self.fLastChannelData[i] = level

# -----------------------------------------------------------------------------------------------------------------

def updateGrandient(self):
@@ -284,6 +298,14 @@ class DigitalPeakMeter(QWidget):
self.fMeterGradient.setColorAt(0.0, self.fMeterColorBase)
self.fMeterGradient.setColorAt(1.0, self.fMeterColorBase)

elif self.fMeterStyle == self.STYLE_TUBE:
color = QColor.fromHslF(0.9, 1, 0.6, 1) # Tuneon filled w/ neon + agron
points = 20
for i in range(points + 1):
self.fMeterGradient.setColorAt(((i-0.3)/points % 1.0), color)
self.fMeterGradient.setColorAt(( i /points ), Qt.black)
self.fMeterGradient.setColorAt(((i+0.3)/points % 1.0), color)

self.updateGrandientFinalStop()

def updateGrandientFinalStop(self):
@@ -360,6 +382,13 @@ class DigitalPeakMeter(QWidget):
meterPad += 2
meterSize -= 2

elif self.fMeterStyle == self.STYLE_TUBE:
painter.setPen(QPen(Qt.NoPen))
painter.setBrush(self.fMeterGradient)
meterPos += 3
meterPad += 6
meterSize -= 6

else:
painter.setPen(QPen(self.fMeterBackground, 0))
painter.setBrush(self.fMeterGradient)


+ 16
- 15
source/frontend/widgets/paramspinbox.py View File

@@ -25,7 +25,7 @@ elif qt_config == 6:
import ui_inputdialog_value

from carla_backend import CARLA_OS_MAC
from carla_shared import countDecimalPoints
from carla_shared import countDecimalPoints, getPrefixSuffix, strLim

# ------------------------------------------------------------------------------------------------------------
# Get a fixed value within min/max bounds
@@ -46,13 +46,21 @@ def geFixedValue(name, value, minimum, maximum):
# Custom InputDialog with Scale Points support

class CustomInputDialog(QDialog):
def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints, prefix, suffix):
def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints, prefix, suffix, unit=""):
QDialog.__init__(self, parent)
self.ui = ui_inputdialog_value.Ui_Dialog()
self.ui.setupUi(self)

decimals = countDecimalPoints(step, stepSmall)
self.ui.label.setText(label)
if not (unit == ""):
prefix, suffix = getPrefixSuffix(unit)

if unit == "%":
decimals = 1
else:
decimals = countDecimalPoints(step, stepSmall)

# self.ui.label.setText(label + " [" + strRound(self, minimum, decimals) + "..." + strRound(self, maximum, decimals) + "]")
self.ui.label.setText(label + " [" + strLim(minimum) + "..." + strLim(maximum) + "]")
self.ui.doubleSpinBox.setDecimals(decimals)
self.ui.doubleSpinBox.setRange(minimum, maximum)
self.ui.doubleSpinBox.setSingleStep(step)
@@ -361,14 +369,7 @@ class ParamSpinBox(QAbstractSpinBox):
self.fStepLarge = value

def setLabel(self, label):
prefix = ""
suffix = label.strip()

if suffix == "(coef)":
prefix = "* "
suffix = ""
else:
suffix = " " + suffix
prefix, suffix = getPrefixSuffix(label)

self.fLabelPrefix = prefix
self.fLabelSuffix = suffix
@@ -533,16 +534,16 @@ class ParamSpinBox(QAbstractSpinBox):
pass

menu = QMenu(self)
actReset = menu.addAction(self.tr("Reset (%f)" % self.fDefault))
actReset = menu.addAction(self.tr("Reset (" + strLim(self.fDefault) + ")"))
actRandom = menu.addAction(self.tr("Random"))
menu.addSeparator()
actCopy = menu.addAction(self.tr("Copy (%f)" % self.fValue))
actCopy = menu.addAction(self.tr("Copy (" + strLim(self.fValue) + ")"))

if pasteValue is None:
actPaste = menu.addAction(self.tr("Paste"))
actPaste.setEnabled(False)
else:
actPaste = menu.addAction(self.tr("Paste (%f)" % pasteValue))
actPaste = menu.addAction(self.tr("Paste (" + strLim(pasteValue) + ")"))

menu.addSeparator()



+ 10
- 4
source/frontend/widgets/racklistwidget.py View File

@@ -13,11 +13,11 @@ import os
from qt_compat import qt_config

if qt_config == 5:
from PyQt5.QtCore import Qt, QSize, QRect, QEvent
from PyQt5.QtCore import QT_VERSION, Qt, QSize, QRect, QEvent
from PyQt5.QtGui import QColor, QPainter, QPixmap
from PyQt5.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMessageBox
elif qt_config == 6:
from PyQt6.QtCore import Qt, QSize, QRect, QEvent
from PyQt6.QtCore import QT_VERSION, Qt, QSize, QRect, QEvent, QPoint # QPoint is for Qt6 only.
from PyQt6.QtGui import QColor, QPainter, QPixmap
from PyQt6.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMessageBox

@@ -291,7 +291,10 @@ class RackListWidget(QListWidget):

event.acceptProposedAction()

tryItem = self.itemAt(event.pos())
if QT_VERSION < 0x60000:
tryItem = self.itemAt(event.pos())
else:
tryItem = self.itemAt(QPoint(int(event.position().x()), int(event.position().y())))

if tryItem is not None:
self.setCurrentRow(tryItem.getPluginId())
@@ -318,7 +321,10 @@ class RackListWidget(QListWidget):
if not urls:
return

tryItem = self.itemAt(event.pos())
if QT_VERSION < 0x60000:
tryItem = self.itemAt(event.pos())
else:
tryItem = self.itemAt(QPoint(int(event.position().x()), int(event.position().y())))

if tryItem is not None:
pluginId = tryItem.getPluginId()


+ 840
- 224
source/frontend/widgets/scalabledial.py
File diff suppressed because it is too large
View File


+ 217
- 98
source/frontend/xycontroller-ui View File

@@ -10,17 +10,21 @@ from qt_compat import qt_config
if qt_config == 5:
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer
from PyQt5.QtGui import QColor, QPainter, QPen
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow, QApplication
elif qt_config == 6:
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer
from PyQt6.QtGui import QColor, QPainter, QPen
from PyQt6.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow
from PyQt6.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow, QApplication

import ctypes
from time import sleep

# -----------------------------------------------------------------------
# Imports (Custom)

from carla_shared import *
from carla_utils import *
from widgets.scalabledial import ScalableDial

import ui_xycontroller

@@ -32,15 +36,21 @@ from externalui import ExternalUI
from widgets.paramspinbox import ParamSpinBox

# ------------------------------------------------------------------------------------------------------------

XYCONTROLLER_PARAMETER_X = 0
XYCONTROLLER_PARAMETER_Y = 1
XYCONTROLLER_PARAMETER_SMOOTH = 0
XYCONTROLLER_PARAMETER_LINEAR = 1
XYCONTROLLER_PARAMETER_SPEED = 2
XYCONTROLLER_PARAMETER_REVERSEY = 3
XYCONTROLLER_PARAMETER_X = 4
XYCONTROLLER_PARAMETER_Y = 5
XYCONTROLLER_PARAMETER_OUT_X = 6
XYCONTROLLER_PARAMETER_OUT_Y = 7

# ------------------------------------------------------------------------------------------------------------

class XYGraphicsScene(QGraphicsScene):
# signals
cursorMoved = pyqtSignal(float,float)
knobsUpdate = pyqtSignal(float,float)

def __init__(self, parent):
QGraphicsScene.__init__(self, parent)
@@ -52,9 +62,20 @@ class XYGraphicsScene(QGraphicsScene):
self.m_channels = []
self.m_mouseLock = False
self.m_smooth = False
self.m_linear = False
self.m_speed = 8.0 # 1.0 to 100.0
self.m_rSmooth = False # Using right button
self.m_smooth_x = 0.0
self.m_smooth_y = 0.0

self.reverseY = 1.0 # -1.0 when reversed

self.xpPrev = 0.0
self.ypPrev = 0.0

self.time: ctypes.c_uint64 = 0 # I sure just int is quite enough here, but...
self.prevTime: ctypes.c_uint64 = 0

self.setBackgroundBrush(Qt.black)

cursorPen = QPen(QColor(255, 255, 255), 2)
@@ -87,31 +108,31 @@ class XYGraphicsScene(QGraphicsScene):
self.m_lineV.setX(posX)

if forward:
value = posX / (self.p_size.x() + self.p_size.width());
value = posX / (self.p_size.x() + self.p_size.width())
self.sendMIDI(value, None)
else:
self.m_smooth_x = posX;
self.m_smooth_x = posX

def setPosY(self, y: float, forward: bool = True):
if self.m_mouseLock:
return;
return

posY = y * (self.p_size.y() + self.p_size.height())
posY = y * (self.p_size.y() + self.p_size.height()) * self.reverseY
self.m_cursor.setPos(self.m_cursor.x(), posY)
self.m_lineH.setY(posY)

if forward:
value = posY / (self.p_size.y() + self.p_size.height())
value = posY / (self.p_size.y() + self.p_size.height()) * self.reverseY
self.sendMIDI(None, value)
else:
self.m_smooth_y = posY

def setSmooth(self, smooth: bool):
self.m_smooth = smooth

def setSmoothValues(self, x: float, y: float):
self.m_smooth_x = x * (self.p_size.x() + self.p_size.width());
self.m_smooth_y = y * (self.p_size.y() + self.p_size.height());
self.m_smooth_x = x * (self.p_size.x() + self.p_size.width())
self.m_smooth_y = y * (self.p_size.y() + self.p_size.height()) * self.reverseY

def setReverseY(self, rev: bool):
self.reverseY = 1 - (int(rev) * 2) # 1.0 or -1.0

# -------------------------------------------------------------------

@@ -119,82 +140,132 @@ class XYGraphicsScene(QGraphicsScene):
self.p_size.setRect(-(float(size.width())/2),
-(float(size.height())/2),
size.width(),
size.height());
size.height())

def updatePos(self, pos: QPointF, filterSame: bool = False, knobsOnly: bool = False):
xp = pos.x() / (self.p_size.x() + self.p_size.width())
yp = pos.y() / (self.p_size.y() + self.p_size.height()) * self.reverseY
if knobsOnly:
self.knobsUpdate.emit(xp * 100, yp * 100)
return

self.m_cursor.setPos(pos)
self.m_lineH.setY(pos.y())
self.m_lineV.setX(pos.x())

# Set 0.05% precision, yet exact final value settling, esp. zero
xp = round(xp * 1000) / 1000
yp = round(yp * 1000) / 1000

self.sendMIDI(xp, yp, filterSame)
self.cursorMoved.emit(xp, yp)

def updateSmooth(self):
if not self.m_smooth:
def updateSmooth(self, time):
if not (self.m_smooth or self.m_rSmooth):
return

if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y:
dx = self.m_smooth_x - self.m_cursor.x()
dy = self.m_smooth_y - self.m_cursor.y()

if dx == dy == 0:
return

same = 0
if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005:
if abs(dx) <= 0.0005:
self.m_smooth_x = self.m_cursor.x()
same += 1

if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005:
if abs(dy) <= 0.0005:
self.m_smooth_y = self.m_cursor.y()
same += 1

if same == 2:
return

newX = float(self.m_smooth_x + self.m_cursor.x()*7) / 8
newY = float(self.m_smooth_y + self.m_cursor.y()*7) / 8
pos = QPointF(newX, newY)
speed = self.m_speed

self.m_cursor.setPos(pos)
self.m_lineH.setY(pos.y())
self.m_lineV.setX(pos.x())
mod = QApplication.keyboardModifiers()
if (mod & Qt.ControlModifier):
speed /= 2
elif (mod & Qt.ShiftModifier):
speed *= 2

xp = pos.x() / (self.p_size.x() + self.p_size.width())
yp = pos.y() / (self.p_size.y() + self.p_size.height())
if self.m_linear:
newX = self.m_cursor.x() + max(min(dx / speed, 1), -1) * speed
newY = self.m_cursor.y() + max(min(dy / speed, 1), -1) * speed
else:
precision = 64 / speed
newX = float(self.m_smooth_x + self.m_cursor.x()*(precision-1)) / precision
newY = float(self.m_smooth_y + self.m_cursor.y()*(precision-1)) / precision

self.sendMIDI(xp, yp)
self.cursorMoved.emit(xp, yp)
pos = QPointF(newX, newY)

self.updatePos(pos, ((time - self.prevTime) == 1)) # Continuous calls or Not

self.prevTime = time

# -------------------------------------------------------------------

def handleMousePos(self, pos: QPointF):
def handleMousePos(self, event):

if (event.buttons() & Qt.MiddleButton):
pos = QPointF(0, 0)
else:
pos = QPointF(event.scenePos())

if not self.p_size.contains(pos):
if pos.x() < self.p_size.x():
pos.setX(self.p_size.x())
elif pos.x() > (self.p_size.x() + self.p_size.width()):
pos.setX(self.p_size.x() + self.p_size.width());
pos.setX(self.p_size.x() + self.p_size.width())

if pos.y() < self.p_size.y():
pos.setY(self.p_size.y())
elif pos.y() > (self.p_size.y() + self.p_size.height()):
pos.setY(self.p_size.y() + self.p_size.height())

self.updatePos(pos, knobsOnly=True)

self.m_smooth_x = pos.x()
self.m_smooth_y = pos.y()

if not self.m_smooth:
self.m_cursor.setPos(pos)
self.m_lineH.setY(pos.y())
self.m_lineV.setX(pos.x())
self.m_rSmooth = event.buttons() & Qt.RightButton

xp = pos.x() / (self.p_size.x() + self.p_size.width());
yp = pos.y() / (self.p_size.y() + self.p_size.height());
# When not smooth, update each time;
# (commented-out part) When smooth, update (re-send same) if click position is same (when smoothing not sends anything). (To match non-smoothed behaviour)
if (not (self.m_smooth or self.m_rSmooth)): # or (abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005 and abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005):
self.updatePos(pos)

self.sendMIDI(xp, yp)
self.cursorMoved.emit(xp, yp)

def sendMIDI(self, xp, yp):
def sendMIDI(self, xp, yp, filterSame = False):
rate = float(0xff) / 4
msgd = ["cc2" if xp is not None and yp is not None else "cc"]
prefix = []
msgd = []

if xp is not None:
msgd.append(self.cc_x)
msgd.append(int(xp * rate + rate))
value = int(xp * rate + rate)
if not (filterSame and (value == self.xpPrev)):
prefix = ["cc"]

msgd.append(self.cc_x)
msgd.append(value)

self.xpPrev = value

if yp is not None:
msgd.append(self.cc_y)
msgd.append(int(yp * rate + rate))
value = int(yp * rate + rate)
if not (filterSame and (value == self.ypPrev)):
if prefix == []:
prefix = ["cc"]
else:
prefix = ["cc2"]

msgd.append(self.cc_y)
msgd.append(value)

self.rparent.send(msgd)
self.ypPrev = value

if not (prefix == []):
self.rparent.send(prefix + msgd)

# -------------------------------------------------------------------

@@ -206,13 +277,13 @@ class XYGraphicsScene(QGraphicsScene):

def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
self.m_mouseLock = True
self.handleMousePos(event.scenePos())
self.handleMousePos(event)
self.rparent.setCursor(Qt.CrossCursor)
QGraphicsScene.mousePressEvent(self, event);
QGraphicsScene.mousePressEvent(self, event)

def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
self.handleMousePos(event.scenePos())
QGraphicsScene.mouseMoveEvent(self, event);
self.handleMousePos(event)
QGraphicsScene.mouseMoveEvent(self, event)

def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
self.m_mouseLock = False
@@ -231,15 +302,22 @@ class XYControllerUI(ExternalUI, QMainWindow):

self.fSaveSizeNowChecker = -1

self.isXActual = False
self.isYActual = False

# ---------------------------------------------------------------
# Set-up GUI stuff

self.scene = XYGraphicsScene(self)

self.ui.dial_x.setImage(2)
self.ui.dial_y.setImage(2)
self.ui.dial_x.setLabel("X")
self.ui.dial_y.setLabel("Y")
# Now knobs are QWidgets, not QDials.
self.ui.dial_x = ScalableDial(self.ui.dial_x, 0, 400, 0, -100, 100, "X", 64, -1, "%", "tube", 1, {})
self.ui.dial_y = ScalableDial(self.ui.dial_y, 1, 400, 0, -100, 100, "Y", 64, -1, "%", "tube", 1, {})

# These are outputs (7-seg displays).
self.ui.dial_out_x = ScalableDial(self.ui.dial_out_x, 2, 400, 0, -100, 100, "Out X", 64, -1, "%", "tube", 1, {'Auto7segWidth':1, }, isOutput=True)
self.ui.dial_out_y = ScalableDial(self.ui.dial_out_y, 3, 400, 0, -100, 100, "Out Y", 64, -1, "%", "tube", 1, {'Auto7segWidth':1, }, isOutput=True)

self.ui.keyboard.setOctaves(10)

self.ui.graphicsView.setScene(self.scene)
@@ -262,6 +340,7 @@ class XYControllerUI(ExternalUI, QMainWindow):
# Connect actions to functions

self.scene.cursorMoved.connect(self.slot_sceneCursorMoved)
self.scene.knobsUpdate.connect(self.slot_setKnobs)

self.ui.keyboard.noteOn.connect(self.slot_noteOn)
self.ui.keyboard.noteOff.connect(self.slot_noteOff)
@@ -271,6 +350,7 @@ class XYControllerUI(ExternalUI, QMainWindow):
self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX)
self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY)


if QT_VERSION >= 0x60000:
self.ui.cb_control_x.currentTextChanged.connect(self.slot_checkCC_X)
self.ui.cb_control_y.currentTextChanged.connect(self.slot_checkCC_Y)
@@ -308,17 +388,23 @@ class XYControllerUI(ExternalUI, QMainWindow):

# -------------------------------------------------------------------

def setSmooth(self, smooth: bool):
self.scene.m_smooth = smooth

x = self.ui.dial_x.rvalue() / 100
y = self.ui.dial_y.rvalue() / 100
if smooth:
self.scene.setSmoothValues(x, y)
else:
self.scene.setPosX(x, True)
self.scene.setPosY(y, True)
self.slot_sceneCursorMoved(x, y)

@pyqtSlot()
def slot_updateScreen(self):
self.ui.graphicsView.centerOn(0, 0)
self.scene.updateSize(self.ui.graphicsView.size())

dial_x = self.ui.dial_x.rvalue()
dial_y = self.ui.dial_y.rvalue()
self.scene.setPosX(dial_x / 100, False)
self.scene.setPosY(dial_y / 100, False)
self.scene.setSmoothValues(dial_x / 100, dial_y / 100)

@pyqtSlot(int)
def slot_noteOn(self, note):
self.send(["note", True, note])
@@ -328,16 +414,34 @@ class XYControllerUI(ExternalUI, QMainWindow):
self.send(["note", False, note])

@pyqtSlot(float)
def slot_knobValueChangedX(self, x: float):
self.sendControl(XYCONTROLLER_PARAMETER_X, x)
self.scene.setPosX(x / 100, True)
self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100)
def slot_knobValueChangedX(self, x:float, external:bool=False, firstRun:bool=False):
if not external:
self.sendControl(XYCONTROLLER_PARAMETER_X, x)
else:
self.ui.dial_x.setValue(x, False)

if (not self.scene.m_smooth) or firstRun:
self.sendControl(XYCONTROLLER_PARAMETER_OUT_X, x)
self.ui.dial_out_x.setValue(x, False)
self.scene.setPosX(x / 100, True)

else:
self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100)

@pyqtSlot(float)
def slot_knobValueChangedY(self, y: float):
self.sendControl(XYCONTROLLER_PARAMETER_Y, y)
self.scene.setPosY(y / 100, True)
self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100)
def slot_knobValueChangedY(self, y:float, external:bool=False, firstRun:bool=False):
if not external:
self.sendControl(XYCONTROLLER_PARAMETER_Y, y)
else:
self.ui.dial_y.setValue(y, False)

if (not self.scene.m_smooth) or firstRun:
self.sendControl(XYCONTROLLER_PARAMETER_OUT_Y, y)
self.ui.dial_out_y.setValue(y, False)
self.scene.setPosY(y / 100, True)

else:
self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100)

@pyqtSlot(str)
def slot_checkCC_X(self, text: str):
@@ -422,20 +526,16 @@ class XYControllerUI(ExternalUI, QMainWindow):

@pyqtSlot(bool)
def slot_setSmooth(self, smooth):
self.scene.setSmooth(smooth)
self.setSmooth(smooth)
self.sendConfigure("smooth", "yes" if smooth else "no")

if smooth:
dial_x = self.ui.dial_x.rvalue()
dial_y = self.ui.dial_y.rvalue()
self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
self.sendControl(XYCONTROLLER_PARAMETER_SMOOTH, int(smooth))

@pyqtSlot(float, float)
def slot_sceneCursorMoved(self, xp: float, yp: float):
self.ui.dial_x.setValue(xp * 100, False)
self.ui.dial_y.setValue(yp * 100, False)
self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100)
self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100)
self.ui.dial_out_x.setValue(xp * 100, False)
self.ui.dial_out_y.setValue(yp * 100, False)
self.sendControl(XYCONTROLLER_PARAMETER_OUT_X, xp * 100)
self.sendControl(XYCONTROLLER_PARAMETER_OUT_Y, yp * 100)

@pyqtSlot(bool)
def slot_showKeyboard(self, yesno):
@@ -443,21 +543,47 @@ class XYControllerUI(ExternalUI, QMainWindow):
self.sendConfigure("show-midi-keyboard", "yes" if yesno else "no")
QTimer.singleShot(0, self.slot_updateScreen)

@pyqtSlot(float, float)
def slot_setKnobs(self, x, y):
self.ui.dial_x.setValue(x, False)
self.ui.dial_y.setValue(y, False)
self.sendControl(XYCONTROLLER_PARAMETER_X, x)
self.sendControl(XYCONTROLLER_PARAMETER_Y, y)

# -------------------------------------------------------------------
# DSP Callbacks

# NOTE It called continuously with params 6, 7 (outs, not used here), is this good?
def dspParameterChanged(self, index: int, value: float):
if index == XYCONTROLLER_PARAMETER_X:
self.ui.dial_x.setValue(value, False)
self.scene.setPosX(value / 100, False)
if index == XYCONTROLLER_PARAMETER_SMOOTH:
self.ui.cb_smooth.blockSignals(True)
self.ui.cb_smooth.setChecked(bool(value))
self.ui.cb_smooth.blockSignals(False)
self.setSmooth(bool(value))

elif index == XYCONTROLLER_PARAMETER_LINEAR:
self.scene.m_linear = bool(value)

elif index == XYCONTROLLER_PARAMETER_SPEED:
self.scene.m_speed = value

elif index == XYCONTROLLER_PARAMETER_REVERSEY:
self.scene.setReverseY(bool(value))

elif index == XYCONTROLLER_PARAMETER_X:
self.slot_knobValueChangedX(value, True, not self.isXActual)
self.isXActual = True

elif index == XYCONTROLLER_PARAMETER_Y:
self.ui.dial_y.setValue(value, False)
self.scene.setPosY(value / 100, False)
self.slot_knobValueChangedY(value, True, not self.isYActual)
if self.isXActual and not self.isYActual:
# Run it once, BUT when both X & Y are received (they can be shuffled).
self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, self.ui.dial_y.rvalue() / 100)
self.isYActual = True

else:
return

self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100,
self.ui.dial_y.rvalue() / 100)

def dspStateChanged(self, key: str, value: str):
if key == "guiWidth":
@@ -480,15 +606,7 @@ class XYControllerUI(ExternalUI, QMainWindow):

elif key == "smooth":
smooth = (value == "yes")
self.ui.cb_smooth.blockSignals(True)
self.ui.cb_smooth.setChecked(smooth)
self.ui.cb_smooth.blockSignals(False)
self.scene.setSmooth(smooth)

if smooth:
dial_x = self.ui.dial_x.rvalue()
dial_y = self.ui.dial_y.rvalue()
self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
self.setSmooth(bool(smooth))

elif key == "show-midi-keyboard":
show = (value == "yes")
@@ -587,9 +705,10 @@ class XYControllerUI(ExternalUI, QMainWindow):
QMainWindow.resizeEvent(self, event)

def timerEvent(self, event):
self.scene.time += 1
if event.timerId() == self.fIdleTimer:
self.idleExternalUI()
self.scene.updateSmooth()
self.scene.updateSmooth(self.scene.time)

if self.fSaveSizeNowChecker == 11:
self.sendConfigure("guiWidth", str(self.width()))


+ 64
- 3
source/native-plugins/xycontroller.cpp View File

@@ -29,6 +29,10 @@ class XYControllerPlugin : public NativePluginAndUiClass
{
public:
enum Parameters {
kParamSmooth,
kParamLinear,
kParamSpeed,
kParamReverseY,
kParamInX,
kParamInY,
kParamOutX,
@@ -44,6 +48,7 @@ public:
mqueueRT()
{
carla_zeroStruct(params);
params[kParamSpeed] = 8.0f;
carla_zeroStruct(channels);
channels[0] = true;
}
@@ -92,6 +97,56 @@ protected:
hints |= NATIVE_PARAMETER_IS_OUTPUT;
param.name = "Out Y";
break;

case kParamSpeed:
param.name = "Speed";
param.unit = "px";
param.ranges.def = 8.0f;
param.ranges.min = 1.0f;
break;
}

if (param.name == nullptr)
{
hints |= NATIVE_PARAMETER_IS_INTEGER | NATIVE_PARAMETER_USES_SCALEPOINTS;
param.unit = nullptr;
param.ranges.min = 0;
param.ranges.max = 1;
param.scalePointCount = 2;

switch (index)
{
case kParamSmooth:
param.name = "Smooth";
{
static const NativeParameterScalePoint scalePoints[2] = {
{ "Thru", 0 },
{ "Smooth", 1 }
};
param.scalePoints = scalePoints;
}
break;
case kParamLinear:
param.name = "Linear";
{
static const NativeParameterScalePoint scalePoints[2] = {
{ "Log", 0 },
{ "Linear", 1 }
};
param.scalePoints = scalePoints;
}
break;
case kParamReverseY:
param.name = "Rev Y";
{
static const NativeParameterScalePoint scalePoints[2] = {
{ "Top to Bottom", 0 },
{ "Bottom to Top", 1 }
};
param.scalePoints = scalePoints;
}
break;
}
}

param.hints = static_cast<NativeParameterHints>(hints);
@@ -113,8 +168,14 @@ protected:
{
switch (index)
{
case kParamSmooth:
case kParamLinear:
case kParamSpeed:
case kParamReverseY:
case kParamInX:
case kParamInY:
case kParamOutX:
case kParamOutY:
params[index] = value;
break;
}
@@ -147,8 +208,8 @@ protected:
void process(const float* const*, float**, const uint32_t,
const NativeMidiEvent* const midiEvents, const uint32_t midiEventCount) override
{
params[kParamOutX] = params[kParamInX];
params[kParamOutY] = params[kParamInY];
// params[kParamOutX] = params[kParamInX];
// params[kParamOutY] = params[kParamInY];

if (mqueue.isNotEmpty() && mqueueRT.tryToCopyDataFrom(mqueue))
{
@@ -266,7 +327,7 @@ static const NativePluginDescriptor notesDesc = {
/* audioOuts */ 0,
/* midiIns */ 1,
/* midiOuts */ 1,
/* paramIns */ 2,
/* paramIns */ 6,
/* paramOuts */ 2,
/* name */ "XY Controller",
/* label */ "xycontroller",


+ 14
- 0
source/rest/carla-host.cpp View File

@@ -994,6 +994,20 @@ void handle_carla_set_panning(const std::shared_ptr<Session> session)
session->close(OK);
}

void handle_carla_set_forth(const std::shared_ptr<Session> session)
{
const std::shared_ptr<const Request> request = session->get_request();

const int pluginId = std::atoi(request->get_query_parameter("pluginId").c_str());
CARLA_SAFE_ASSERT_RETURN(pluginId >= 0,)

const double value = std::atof(request->get_query_parameter("value").c_str());
CARLA_SAFE_ASSERT_RETURN(value >= -1.0 && value <= 1.0,)

carla_set_forth(pluginId, value);
session->close(OK);
}

void handle_carla_set_ctrl_channel(const std::shared_ptr<Session> session)
{
const std::shared_ptr<const Request> request = session->get_request();


+ 1
- 0
source/rest/rest-server.cpp View File

@@ -407,6 +407,7 @@ int main(int, const char**)
make_resource(service, "/set_balance_left", handle_carla_set_balance_left);
make_resource(service, "/set_balance_right", handle_carla_set_balance_right);
make_resource(service, "/set_panning", handle_carla_set_panning);
make_resource(service, "/set_forth", handle_carla_set_forth);
make_resource(service, "/set_ctrl_channel", handle_carla_set_ctrl_channel);
make_resource(service, "/set_option", handle_carla_set_option);



+ 2
- 0
source/utils/CarlaBackendUtils.hpp View File

@@ -196,6 +196,8 @@ const char* InternalParameterIndex2Str(const InternalParameterIndex index) noexc
return "PARAMETER_BALANCE_RIGHT";
case PARAMETER_PANNING:
return "PARAMETER_PANNING";
case PARAMETER_FORTH:
return "PARAMETER_FORTH";
case PARAMETER_CTRL_CHANNEL:
return "PARAMETER_CTRL_CHANNEL";
#endif


+ 8
- 0
source/utils/CarlaStateUtils.cpp View File

@@ -185,6 +185,7 @@ CarlaStateSave::CarlaStateSave() noexcept
balanceLeft(-1.0f),
balanceRight(1.0f),
panning(0.0f),
forth(0.0f),
ctrlChannel(-1),
#endif
currentProgramIndex(-1),
@@ -243,6 +244,7 @@ void CarlaStateSave::clear() noexcept
balanceLeft = -1.0f;
balanceRight = 1.0f;
panning = 0.0f;
forth = 0.0f;
ctrlChannel = -1;
#endif

@@ -346,6 +348,10 @@ bool CarlaStateSave::fillFromXmlElement(const XmlElement* const xmlElement)
{
panning = carla_fixedValue(-1.0f, 1.0f, text.getFloatValue());
}
else if (tag == "Forth")
{
forth = carla_fixedValue(-1.0f, 1.0f, text.getFloatValue());
}
else if (tag == "ControlChannel")
{
if (! text.startsWithIgnoreCase("n"))
@@ -627,6 +633,8 @@ void CarlaStateSave::dumpToMemoryStream(MemoryOutputStream& content) const
dataXml << " <Balance-Right>" << water::String(balanceRight, 7) << "</Balance-Right>\n";
if (carla_isNotEqual(panning, 0.0f))
dataXml << " <Panning>" << water::String(panning, 7) << "</Panning>\n";
if (carla_isNotEqual(forth, 0.0f))
dataXml << " <Forth>" << water::String(forth, 7) << "</Forth>\n";

if (ctrlChannel < 0)
dataXml << " <ControlChannel>N</ControlChannel>\n";


+ 1
- 0
source/utils/CarlaStateUtils.hpp View File

@@ -69,6 +69,7 @@ struct CarlaStateSave {
float balanceLeft;
float balanceRight;
float panning;
float forth;
int8_t ctrlChannel;
#endif



Loading…
Cancel
Save