Browse Source

Merge 5e505428f3 into 12bc40fd6c

pull/2010/merge
Filipe Coelho GitHub 1 month ago
parent
commit
3d580b9ed9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
44 changed files with 2890 additions and 929 deletions
  1. BIN
      resources/16x16/add-from-favorites.svgz
  2. BIN
      resources/16x16/add-jack-alt.svgz
  3. BIN
      resources/16x16/add-jack.svgz
  4. BIN
      resources/16x16/audio-volume-medium.svgz
  5. BIN
      resources/16x16/audio-volume-muted-orig.svgz
  6. BIN
      resources/16x16/audio-volume-muted.svgz
  7. BIN
      resources/16x16/balance-alt.svgz
  8. BIN
      resources/16x16/balance-alt2.svgz
  9. BIN
      resources/16x16/balance.svgz
  10. BIN
      resources/16x16/compact-alt.svgz
  11. BIN
      resources/16x16/compact.svgz
  12. BIN
      resources/16x16/dry.svgz
  13. BIN
      resources/16x16/emblem-favorite.svgz
  14. BIN
      resources/16x16/restore-alt.svgz
  15. BIN
      resources/16x16/restore.svgz
  16. BIN
      resources/16x16/style-alt.svgz
  17. BIN
      resources/16x16/style.svgz
  18. BIN
      resources/16x16/system-shutdown.svgz
  19. BIN
      resources/16x16/system-turnon.svgz
  20. BIN
      resources/16x16/view-refresh-purple.svgz
  21. BIN
      resources/16x16/wet.svgz
  22. +14
    -0
      resources/resources.qrc
  23. +101
    -149
      resources/ui/carla_edit.ui
  24. +99
    -1
      resources/ui/carla_host.ui
  25. +21
    -0
      resources/ui/carla_settings.ui
  26. +90
    -14
      resources/ui/xycontroller.ui
  27. +28
    -4
      source/backend/plugin/CarlaPluginFluidSynth.cpp
  28. +32
    -1
      source/backend/plugin/CarlaPluginLADSPADSSI.cpp
  29. +32
    -1
      source/backend/plugin/CarlaPluginLV2.cpp
  30. +33
    -2
      source/backend/plugin/CarlaPluginNative.cpp
  31. +10
    -0
      source/frontend/carla_backend.py
  32. +146
    -13
      source/frontend/carla_host.py
  33. +7
    -0
      source/frontend/carla_settings.py
  34. +62
    -8
      source/frontend/carla_shared.py
  35. +394
    -203
      source/frontend/carla_skin.py
  36. +227
    -82
      source/frontend/carla_widgets.py
  37. +5
    -3
      source/frontend/widgets/collapsablewidget.py
  38. +406
    -96
      source/frontend/widgets/commondial.py
  39. +37
    -8
      source/frontend/widgets/digitalpeakmeter.py
  40. +16
    -15
      source/frontend/widgets/paramspinbox.py
  41. +10
    -4
      source/frontend/widgets/racklistwidget.py
  42. +839
    -224
      source/frontend/widgets/scalabledial.py
  43. +217
    -98
      source/frontend/xycontroller-ui
  44. +64
    -3
      source/native-plugins/xycontroller.cpp

BIN
resources/16x16/add-from-favorites.svgz View File


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/style-alt.svgz View File


BIN
resources/16x16/style.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/style.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/style.svgz</normaloff>:/16x16/style.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>


+ 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) && (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;
}
}



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

@@ -1271,6 +1271,9 @@ public:

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

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

// extra plugin hints
@@ -2083,7 +2086,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)
{
@@ -2115,10 +2122,34 @@ public:
}
}

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

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

// Pan: Stereo, 3 ch (extra rear/bass), or Quadro.
if ((pan != 0.0) && ((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);
}
}

// 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;
}
}



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

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

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

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

@@ -4684,7 +4687,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)
{
@@ -4716,10 +4723,34 @@ public:
}
}

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

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

// Pan: Stereo, 3 ch (extra rear/bass), or Quadro.
if ((pan != 0.0) && ((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);
}
}

// 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


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

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

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

// native plugin hints
if (fDescriptor->hints & NATIVE_PLUGIN_IS_RTSAFE)
pData->hints |= PLUGIN_IS_RTSAFE;
@@ -2369,7 +2372,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 +2383,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 +2419,34 @@ public:
}
}

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

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

// Pan: Stereo, 3 ch (extra rear/bass), or Quadro.
if ((pan != 0.0) && ((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);
}
}

// 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;
}
}



+ 10
- 0
source/frontend/carla_backend.py View File

@@ -310,6 +310,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.


+ 146
- 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):
@@ -1502,6 +1547,38 @@ class HostWindow(QMainWindow):
pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_RIGHT, 1.0)
pitem.getWidget().setInternalParameter(PARAMETER_PANNING, 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):
for pitem in self.fPluginList:
@@ -1531,11 +1608,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 +1648,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 +1660,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 +2068,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 +2103,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 +2173,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 +2290,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 +2744,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 +3012,6 @@ class HostWindow(QMainWindow):

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

if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins:
return
@@ -2922,6 +3044,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 +3075,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")


+ 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)


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

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

from math import fmod
from math import fmod, log10

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

from utils import QSafeSettings
import ast

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

@@ -184,6 +187,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 +277,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 +652,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 +936,50 @@ 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):
# np.format_float_positional(value, trim='-', fractional=False, precision=digits)
result = "%.5f" % value
if "." in result:
result = result.strip("0")
if result[-1] == ".":
result = result.removesuffix(".")

if len(result) > 9:
return '{:.3e}'.format(value)
else:
return result

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

RACK_KNOB_GAP = 5

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

+ 394
- 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,145 @@ 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_VOLUME, widget])
self.w_knobs_right.layout().addWidget(widget)
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)

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)
@@ -724,6 +896,7 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta):
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 +1064,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 +1083,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 +1185,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 +1228,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 +1251,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 +1365,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 +1484,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 +1546,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 +1840,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 +1883,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 +1979,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 +1999,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 +2147,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 +2204,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 +2218,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 +2232,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


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

@@ -14,7 +14,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 +24,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 +45,14 @@ elif qt_config == 6:
QScrollArea,
QVBoxLayout,
QWidget,
QGraphicsScene,
QGraphicsTextItem,
QGraphicsView,
QTableWidget,
QTableWidgetItem,
QHeaderView,
QLabel,
QSizePolicy,
)

# ------------------------------------------------------------------------------------------------------------
@@ -72,6 +88,7 @@ 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,
@@ -84,6 +101,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 +112,7 @@ from carla_backend import (
from carla_shared import (
MIDI_CC_LIST, MAX_MIDI_CC_LIST_ITEM,
countDecimalPoints,
strLim,
fontMetricsHorizontalAdvance,
setUpSignals,
gCarla
@@ -103,6 +122,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 +140,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 +163,7 @@ class PluginParameter(QWidget):
self.fParameterId = pInfo['index']
self.fPluginId = pluginId
self.fTabIndex = tabIndex
self.fKnobVisible = True

# -------------------------------------------------------------
# Set-up GUI
@@ -157,6 +180,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,39 +560,21 @@ 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.sb_ctrl_channel.setValue(self.fControlChannel+1)
@@ -582,10 +588,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
@@ -901,7 +907,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 +979,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)
@@ -1474,69 +1485,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):
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 +1614,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 +1689,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)



+ 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)


+ 406
- 96
source/frontend/widgets/commondial.py View File

@@ -1,22 +1,26 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
# SPDX-FileCopyrightText: 2011-2025 Filipe Coelho <falktx@falktx.com>
# SPDX-License-Identifier: GPL-2.0-or-later

# ---------------------------------------------------------------------------------------------------------------------
# Imports (Global)

from math import isnan
from math import isnan, log10

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

# ---------------------------------------------------------------------------------------------------------------------
# Widget Class
@@ -25,7 +29,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 +37,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 +57,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 +96,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 +121,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 +201,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): # -3, -4, -7
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): # -3, -4, -7

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 +464,118 @@ 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"
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()


+ 839
- 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",


Loading…
Cancel
Save