diff --git a/.circleci/config.yml b/.circleci/config.yml index a3be541b592..f7f509d7e90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -178,7 +178,10 @@ jobs: - *restore_cache - run: name: Install Homebrew dependencies - command: brew update && brew install ccache fftw cmake pkg-config libogg libvorbis lame libsndfile libsamplerate jack sdl libgig libsoundio stk fluid-synth portaudio fltk qt5 carla + command: | + # unlink Homebrew's python 2 to prevent an node-gyp error + brew unlink python@2 || true + brew update && brew install ccache fftw cmake pkg-config libogg libvorbis lame libsndfile libsamplerate jack sdl libgig libsoundio stk fluid-synth portaudio fltk qt5 carla - run: name: Install nodejs dependencies command: npm install -g appdmg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2c22146ae3f..fcc87560172 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -19,7 +19,13 @@ assignees: '' #### Screenshot -#### LMMS version used +#### Affected LMMS versions + + #### Logs
diff --git a/.gitmodules b/.gitmodules index 28d6c5d46de..56b7f3eabb3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,3 +34,6 @@ [submodule "doc/wiki"] path = doc/wiki url = https://github.com/lmms/lmms.wiki.git +[submodule "src/3rdparty/ringbuffer"] + path = src/3rdparty/ringbuffer + url = https://github.com/JohannesLorenz/ringbuffer.git diff --git a/.travis/osx..before_install.sh b/.travis/osx..before_install.sh index b59920a5eef..61f25af66ff 100755 --- a/.travis/osx..before_install.sh +++ b/.travis/osx..before_install.sh @@ -3,3 +3,5 @@ set -e brew update +# Python 2 may cause conflicts on dependency installation +brew unlink python@2 || true diff --git a/.travis/osx..install.sh b/.travis/osx..install.sh index e3dd670bfb2..93d478c40f4 100755 --- a/.travis/osx..install.sh +++ b/.travis/osx..install.sh @@ -2,7 +2,7 @@ set -e -PACKAGES="cmake pkg-config libogg libvorbis lame libsndfile libsamplerate jack sdl libgig libsoundio stk fluid-synth portaudio node fltk qt5 carla" +PACKAGES="cmake pkg-config libogg libvorbis lame libsndfile libsamplerate jack sdl libgig libsoundio stk fluid-synth portaudio node fltk qt carla" if "${TRAVIS}"; then PACKAGES="$PACKAGES ccache" diff --git a/cmake/modules/FindSDL2.cmake b/cmake/modules/FindSDL2.cmake index 89aea7a8a03..7b9e5b14977 100644 --- a/cmake/modules/FindSDL2.cmake +++ b/cmake/modules/FindSDL2.cmake @@ -82,7 +82,7 @@ SET(SDL2_SEARCH_PATHS FIND_PATH(SDL2_INCLUDE_DIR SDL.h HINTS $ENV{SDL2DIR} - PATH_SUFFIXES include/SDL2 include + PATH_SUFFIXES SDL2 include/SDL2 include PATHS ${SDL2_SEARCH_PATHS} ) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index c82bba3291a..2d853038873 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -64,6 +64,7 @@ SET(LMMS_PLUGIN_LIST VstEffect watsyn waveshaper + Vectorscope vibed Xpressive zynaddsubfx diff --git a/data/presets/X-Pressive/Ambition.xpf b/data/presets/X-Pressive/Ambition.xpf deleted file mode 100644 index 2d93f7c052f..00000000000 --- a/data/presets/X-Pressive/Ambition.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Baby Violin.xpf b/data/presets/X-Pressive/Baby Violin.xpf deleted file mode 100644 index 2e887d3d2b5..00000000000 --- a/data/presets/X-Pressive/Baby Violin.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Bad Singer.xpf b/data/presets/X-Pressive/Bad Singer.xpf deleted file mode 100644 index ca9cfd5a333..00000000000 --- a/data/presets/X-Pressive/Bad Singer.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Cloud Bass.xpf b/data/presets/X-Pressive/Cloud Bass.xpf deleted file mode 100644 index 4e444f22a90..00000000000 --- a/data/presets/X-Pressive/Cloud Bass.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Creature.xpf b/data/presets/X-Pressive/Creature.xpf deleted file mode 100644 index b667a9c7f7a..00000000000 --- a/data/presets/X-Pressive/Creature.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Electric Shock.xpf b/data/presets/X-Pressive/Electric Shock.xpf deleted file mode 100644 index 7dea6fe4ac9..00000000000 --- a/data/presets/X-Pressive/Electric Shock.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Faded Colors.xpf b/data/presets/X-Pressive/Faded Colors.xpf deleted file mode 100644 index 84a37826a56..00000000000 --- a/data/presets/X-Pressive/Faded Colors.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Fat Flute.xpf b/data/presets/X-Pressive/Fat Flute.xpf deleted file mode 100644 index 92242114ec5..00000000000 --- a/data/presets/X-Pressive/Fat Flute.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Horn.xpf b/data/presets/X-Pressive/Horn.xpf deleted file mode 100644 index 09948056960..00000000000 --- a/data/presets/X-Pressive/Horn.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Piano-Gong.xpf b/data/presets/X-Pressive/Piano-Gong.xpf deleted file mode 100644 index 241f61a550d..00000000000 --- a/data/presets/X-Pressive/Piano-Gong.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Rubber Bass.xpf b/data/presets/X-Pressive/Rubber Bass.xpf deleted file mode 100644 index 73c3648bafc..00000000000 --- a/data/presets/X-Pressive/Rubber Bass.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Space Echoes.xpf b/data/presets/X-Pressive/Space Echoes.xpf deleted file mode 100644 index 1d4d2b543b6..00000000000 --- a/data/presets/X-Pressive/Space Echoes.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Speaker Swapper.xpf b/data/presets/X-Pressive/Speaker Swapper.xpf deleted file mode 100644 index cf80b930468..00000000000 --- a/data/presets/X-Pressive/Speaker Swapper.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Toss.xpf b/data/presets/X-Pressive/Toss.xpf deleted file mode 100644 index 27a0b3f96b9..00000000000 --- a/data/presets/X-Pressive/Toss.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Untuned Bell.xpf b/data/presets/X-Pressive/Untuned Bell.xpf deleted file mode 100644 index 74492706359..00000000000 --- a/data/presets/X-Pressive/Untuned Bell.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Vibrato.xpf b/data/presets/X-Pressive/Vibrato.xpf deleted file mode 100644 index 34795de1194..00000000000 --- a/data/presets/X-Pressive/Vibrato.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/X-Distorted.xpf b/data/presets/X-Pressive/X-Distorted.xpf deleted file mode 100644 index cbe3742a55f..00000000000 --- a/data/presets/X-Pressive/X-Distorted.xpf +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/presets/X-Pressive/Accordion.xpf b/data/presets/Xpressive/Accordion.xpf similarity index 100% rename from data/presets/X-Pressive/Accordion.xpf rename to data/presets/Xpressive/Accordion.xpf diff --git a/data/presets/Xpressive/Ambition.xpf b/data/presets/Xpressive/Ambition.xpf new file mode 100644 index 00000000000..dd64489779c --- /dev/null +++ b/data/presets/Xpressive/Ambition.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Baby Violin.xpf b/data/presets/Xpressive/Baby Violin.xpf new file mode 100644 index 00000000000..45e407fc87f --- /dev/null +++ b/data/presets/Xpressive/Baby Violin.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Bad Singer.xpf b/data/presets/Xpressive/Bad Singer.xpf new file mode 100644 index 00000000000..10fe3b30837 --- /dev/null +++ b/data/presets/Xpressive/Bad Singer.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Cloud Bass.xpf b/data/presets/Xpressive/Cloud Bass.xpf new file mode 100644 index 00000000000..15bf4188daa --- /dev/null +++ b/data/presets/Xpressive/Cloud Bass.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Creature.xpf b/data/presets/Xpressive/Creature.xpf new file mode 100644 index 00000000000..bee39f224fb --- /dev/null +++ b/data/presets/Xpressive/Creature.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/X-Pressive/Dream.xpf b/data/presets/Xpressive/Dream.xpf similarity index 100% rename from data/presets/X-Pressive/Dream.xpf rename to data/presets/Xpressive/Dream.xpf diff --git a/data/presets/Xpressive/Electric Shock.xpf b/data/presets/Xpressive/Electric Shock.xpf new file mode 100644 index 00000000000..3f9aef10429 --- /dev/null +++ b/data/presets/Xpressive/Electric Shock.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Faded Colors - notes test.xpf b/data/presets/Xpressive/Faded Colors - notes test.xpf new file mode 100644 index 00000000000..de4938f4dc4 --- /dev/null +++ b/data/presets/Xpressive/Faded Colors - notes test.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Faded Colors.xpf b/data/presets/Xpressive/Faded Colors.xpf new file mode 100644 index 00000000000..a514ee43837 --- /dev/null +++ b/data/presets/Xpressive/Faded Colors.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Fat Flute.xpf b/data/presets/Xpressive/Fat Flute.xpf new file mode 100644 index 00000000000..76d9e2f84d9 --- /dev/null +++ b/data/presets/Xpressive/Fat Flute.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/X-Pressive/Frog.xpf b/data/presets/Xpressive/Frog.xpf similarity index 100% rename from data/presets/X-Pressive/Frog.xpf rename to data/presets/Xpressive/Frog.xpf diff --git a/data/presets/Xpressive/Horn.xpf b/data/presets/Xpressive/Horn.xpf new file mode 100644 index 00000000000..d44b332b2ab --- /dev/null +++ b/data/presets/Xpressive/Horn.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/X-Pressive/Low Battery.xpf b/data/presets/Xpressive/Low Battery.xpf similarity index 64% rename from data/presets/X-Pressive/Low Battery.xpf rename to data/presets/Xpressive/Low Battery.xpf index c0e648ac908..78f1fc78f72 100644 --- a/data/presets/X-Pressive/Low Battery.xpf +++ b/data/presets/Xpressive/Low Battery.xpf @@ -1,20 +1,21 @@ - + - - + + - + + + - - - - + + + + - - - + + diff --git a/data/presets/Xpressive/Piano-Gong.xpf b/data/presets/Xpressive/Piano-Gong.xpf new file mode 100644 index 00000000000..a8244b7994f --- /dev/null +++ b/data/presets/Xpressive/Piano-Gong.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Rubber Bass.xpf b/data/presets/Xpressive/Rubber Bass.xpf new file mode 100644 index 00000000000..4b1409e224f --- /dev/null +++ b/data/presets/Xpressive/Rubber Bass.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Space Echoes.xpf b/data/presets/Xpressive/Space Echoes.xpf new file mode 100644 index 00000000000..be6de3653b1 --- /dev/null +++ b/data/presets/Xpressive/Space Echoes.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Speaker Swapper.xpf b/data/presets/Xpressive/Speaker Swapper.xpf new file mode 100644 index 00000000000..d4da5aa2f87 --- /dev/null +++ b/data/presets/Xpressive/Speaker Swapper.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Toss.xpf b/data/presets/Xpressive/Toss.xpf new file mode 100644 index 00000000000..387e78fd92f --- /dev/null +++ b/data/presets/Xpressive/Toss.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Untuned Bell.xpf b/data/presets/Xpressive/Untuned Bell.xpf new file mode 100644 index 00000000000..5dd61ec18d1 --- /dev/null +++ b/data/presets/Xpressive/Untuned Bell.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/Vibrato.xpf b/data/presets/Xpressive/Vibrato.xpf new file mode 100644 index 00000000000..a7dda25e9bb --- /dev/null +++ b/data/presets/Xpressive/Vibrato.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/Xpressive/X-Distorted.xpf b/data/presets/Xpressive/X-Distorted.xpf new file mode 100644 index 00000000000..b42495d759d --- /dev/null +++ b/data/presets/Xpressive/X-Distorted.xpf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/presets/ZynAddSubFX/Companion/0074-Smooth Expanded.xiz b/data/presets/ZynAddSubFX/Companion/0074-Smooth Expanded.xiz index cc060f775e4..6ef773a3375 100644 --- a/data/presets/ZynAddSubFX/Companion/0074-Smooth Expanded.xiz +++ b/data/presets/ZynAddSubFX/Companion/0074-Smooth Expanded.xiz @@ -19,7 +19,7 @@ Will Godfrey GPL V 2 or later Now has a slow long tail. -Only really noticable on lower notes. +Only really noticeable on lower notes. diff --git a/debian/control b/debian/control index 463353df0ef..880e0d89ad6 100644 --- a/debian/control +++ b/debian/control @@ -37,18 +37,28 @@ Build-Depends: qttools5-dev, wine64-tools [amd64] | wine32-tools [i386] Standards-Version: 4.2.1.4 -Homepage: http://lmms.io/ +Homepage: https://lmms.io/ Vcs-Browser: https://salsa.debian.org/debian-edu-pkg-team/lmms.git Package: lmms-bin Architecture: any -Depends: lmms-common (>= ${source:Version}), ${shlibs:Depends}, ${misc:Depends}, - stk -Recommends: tap-plugins, caps, +Depends: + lmms-common (>= ${source:Version}), + ${shlibs:Depends}, + ${misc:Depends}, + stk, +Recommends: + caps, lmms-vst-server:i386 (>= ${source:Version}), - lmms-vst-server:amd64 (>= ${source:Version}) -Suggests: fil-plugins, mcp-plugins, omins, freepats, fluid-soundfont-gm, - ladspa-plugin + lmms-vst-server:amd64 (>= ${source:Version}), + tap-plugins, +Suggests: + fil-plugins, + fluid-soundfont-gm, + freepats, + ladspa-plugin, + mcp-plugins, + omins, Replaces: lmms-common (<< 1.0.0-1) Breaks: lmms-common (<< 1.0.0-1) Multi-Arch: allowed @@ -67,7 +77,9 @@ Description: Linux Multimedia Studio - minimal installation Package: lmms Architecture: any -Depends: lmms-bin, ${misc:Depends} +Depends: + lmms-bin, + ${misc:Depends}, Description: Linux Multimedia Studio LMMS aims to be a free alternative to popular (but commercial and closed- source) programs like FruityLoops, Cubase and Logic giving you the ability of @@ -83,7 +95,10 @@ Description: Linux Multimedia Studio Package: lmms-common Architecture: all -Depends: zynaddsubfx-data, ${shlibs:Depends}, ${misc:Depends} +Depends: + ${shlibs:Depends}, + ${misc:Depends}, + zynaddsubfx-data, Pre-Depends: ${misc:Pre-Depends} Description: Linux Multimedia Studio - common files LMMS aims to be a free alternative to popular (but commercial and closed- @@ -101,7 +116,10 @@ Description: Linux Multimedia Studio - common files Package: lmms-vst-server Architecture: amd64 i386 -Depends: wine64 [amd64] | wine64-development [amd64] | wine32 [i386] | wine32-development [i386], ${shlibs:Depends}, ${misc:Depends} +Depends: + wine64 [amd64] | wine64-development [amd64] | wine32 [i386] | wine32-development [i386], + ${shlibs:Depends}, + ${misc:Depends}, Recommends: lmms-bin:any Description: Linux Multimedia Studio - VST server This package contains a helper application that loads VST plugins. diff --git a/debian/copyright b/debian/copyright index 3fbf0917ee1..01b30459ea7 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: LMMS Upstream-Contact: https://github.com/LMMS/lmms Source: https://github.com/LMMS/lmms/tags @@ -1367,7 +1367,7 @@ License: WOL documentation for any purpose is hereby granted without fee, provided that the above copyright notice and this license appear in all source copies. THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF - ANY KIND. See http://www.dspguru.com/wol.htm for more information. + ANY KIND. See https://www.dspguru.com/wol.htm for more information. License: non-free This license does not comply with Debian Free Software Guidelines. diff --git a/debian/patches/contributors.patch b/debian/patches/contributors.patch deleted file mode 100644 index d2b55fd9ac4..00000000000 --- a/debian/patches/contributors.patch +++ /dev/null @@ -1,89 +0,0 @@ -Description: Add contributors - The list of contributors is missing from the source tarball. This list is - generated from upstream repository, running: - git shortlog -sne v1.1.3 | cut -c8- - See https://github.com/LMMS/lmms/issues/2914 for more information. -Author: Javier Serrano Polo - -Index: lmms-1.1.3/doc/CONTRIBUTORS -=================================================================== ---- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ lmms-1.1.3/doc/CONTRIBUTORS 2016-07-12 00:41:47.000000000 +0200 -@@ -0,0 +1,77 @@ -+Tobias Doerffel -+Vesa -+Javier Serrano Polo -+Paul Giblock -+Tres Finocchiaro -+Lukas W -+Raine M. Ekman -+Wong Cho Ching -+Hannu Haahti -+Danny McRae -+Dave French -+Daniel Winzen -+Andreas Brandmaier -+Andrew Kelley -+Oskar Wallgren -+Mike Choi -+Alexandre Almeida -+NoiseByNorthwest -+Johannes Lorenz -+Stian Jørgensrud -+falkTX -+Csaba Hruska -+StakeoutPunch -+ma2moto -+mikobuntu -+8tab <8tab@wp.pl> -+Matthew Krafczyk -+Spekular -+Umcaruje -+DeRobyJ -+Jonathan Aquilina -+ra -+wongcc966422 -+Gurjot Singh -+Janne Sinisalo -+Krzysztof Foltman -+Lou Herard -+Paul Wayper -+Rüdiger Ranft -+Yann Collette -+grindhold -+midi-pascal -+unfa -+Ian Sannar -+Jaroslav Petrnoušek -+LYF610400210 -+Rafael Ruggiero -+psyomn -+quadro -+sarahkeefe -+Achim Settelmeier -+André Hentschel -+Armin Kazmi -+Attila Herman -+Christopher A. Oliver -+Devin Venable -+Fastigium -+Frank Mather -+Frederik -+Hexasoft -+Jens Lang -+Jesse Dubay -+Joel Muzzerall -+Kristi -+Markus Elfring -+Nikos Chantziaras -+Paul Nasca -+Peter Nelson -+Ra -+Steffen Baranowsky -+Thorsten Müller -+TonyChyi -+devin -+dnl-music -+fundamental -+groboclown -+zm1990s diff --git a/debian/patches/series b/debian/patches/series index aba1af04416..94ae11454e2 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1,3 +1,2 @@ -contributors.patch clang.patch build-amd64-20181013.patch diff --git a/include/custom_events.h b/include/ColorChooser.h similarity index 56% rename from include/custom_events.h rename to include/ColorChooser.h index 955521490de..fe5b7a22a4e 100644 --- a/include/custom_events.h +++ b/include/ColorChooser.h @@ -1,7 +1,6 @@ -/* - * custom_events.h - custom event types list +/* ColorChooser.h - declaration and definition of ColorChooser class. * - * Copyright (c) 2007 Javier Serrano Polo + * Copyright (c) 2019 CYBERDEViLNL * * This file is part of LMMS - https://lmms.io * @@ -22,25 +21,21 @@ * */ +#include +#include +#include -#ifndef CUSTOM_EVENTS_H -#define CUSTOM_EVENTS_H - - -#include - - -namespace customEvents +class ColorChooser: public QColorDialog { +public: + ColorChooser(const QColor &initial, QWidget *parent): QColorDialog(initial, parent) {}; + ColorChooser(QWidget *parent): QColorDialog(parent) {}; - enum Type +protected: + // Forward key events to the parent to prevent stuck notes when the dialog gets focus + void keyReleaseEvent(QKeyEvent *event) override { - GUI_UPDATE = QEvent::User - } ; - -} - - - - -#endif + QKeyEvent ke(*event); + QApplication::sendEvent(parentWidget(), &ke); + } +}; diff --git a/include/ControlLayout.h b/include/ControlLayout.h new file mode 100644 index 00000000000..625b3f4681d --- /dev/null +++ b/include/ControlLayout.h @@ -0,0 +1,133 @@ +/* + * ControlLayout.h - layout for controls + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef CONTROLLAYOUT_H +#define CONTROLLAYOUT_H + +#include +#include +#include +class QLayoutItem; +class QRect; +class QString; + +/** + Layout for controls (models) + + Originally token from Qt's FlowLayout example. Modified. + + Features a search bar, as well as looking up widgets with string keys + Keys have to be provided in the widgets' objectNames +*/ +class ControlLayout : public QLayout +{ + Q_OBJECT + +public: + explicit ControlLayout(QWidget *parent, + int margin = -1, int hSpacing = -1, int vSpacing = -1); + ~ControlLayout() override; + + void addItem(QLayoutItem *item) override; + int horizontalSpacing() const; + int verticalSpacing() const; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int) const override; + int count() const override; + QLayoutItem *itemAt(int index) const override; + QLayoutItem *itemByString(const QString& key) const; + QSize minimumSize() const override; + void setGeometry(const QRect &rect) override; + QSize sizeHint() const override; + QLayoutItem *takeAt(int index) override; + +private slots: + void onTextChanged(const QString&); + +private: + int doLayout(const QRect &rect, bool testOnly) const; + int smartSpacing(QStyle::PixelMetric pm) const; + QMap::const_iterator pairAt(int index) const; + + QMultiMap m_itemMap; + int m_hSpace; + int m_vSpace; + // relevant dimension is width, as later, heightForWidth() will be called + // 400 looks good and is ~4 knobs in a row + constexpr const static int m_minWidth = 400; + class QLineEdit* m_searchBar; + //! name of search bar, must be ASCII sorted before any alpha numerics + static constexpr const char* s_searchBarName = "!!searchBar!!"; +}; + +#endif // CONTROLLAYOUT_H diff --git a/include/Controls.h b/include/Controls.h new file mode 100644 index 00000000000..236abbc1199 --- /dev/null +++ b/include/Controls.h @@ -0,0 +1,134 @@ +/* + * Controls.h - labeled control widgets + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef CONTROLS_H +#define CONTROLS_H + + +#include "Model.h" + +// headers only required for covariance +#include "AutomatableModel.h" +#include "ComboBoxModel.h" + + +class QString; +class QWidget; +class AutomatableModel; + + +/** + These classes provide + - a control with a text label + - a type safe way to set a model + (justification: setting the wrong typed model to a widget will cause + hard-to-find runtime errors) +*/ +class Control +{ +public: + virtual QWidget* topWidget() = 0; + virtual void setText(const QString& text) = 0; + + virtual void setModel(AutomatableModel* model) = 0; + virtual AutomatableModel* model() = 0; + virtual class AutomatableModelView* modelView() = 0; + + virtual ~Control(); +}; + + +class KnobControl : public Control +{ + class Knob* m_knob; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + FloatModel* model() override; + class AutomatableModelView* modelView() override; + + KnobControl(QWidget* parent = nullptr); + ~KnobControl() override; +}; + + +class ComboControl : public Control +{ + QWidget* m_widget; + class ComboBox* m_combo; + class QLabel* m_label; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override { return m_widget; } + + void setModel(AutomatableModel* model) override; + ComboBoxModel* model() override; + class AutomatableModelView* modelView() override; + + ComboControl(QWidget* parent = nullptr); + ~ComboControl() override; +}; + + +class LcdControl : public Control +{ + class LcdSpinBox* m_lcd; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + IntModel* model() override; + class AutomatableModelView* modelView() override; + + LcdControl(int numDigits, QWidget* parent = nullptr); + ~LcdControl() override; +}; + + +class CheckControl : public Control +{ + QWidget* m_widget; + class LedCheckBox* m_checkBox; + QLabel* m_label; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + BoolModel *model() override; + class AutomatableModelView* modelView() override; + + CheckControl(QWidget* parent = nullptr); + ~CheckControl() override; +}; + + +#endif // CONTROLS_H diff --git a/include/FadeButton.h b/include/FadeButton.h index 09a4c645791..57d8ba1e693 100644 --- a/include/FadeButton.h +++ b/include/FadeButton.h @@ -50,7 +50,6 @@ public slots: protected: - void customEvent( QEvent * ) override; void paintEvent( QPaintEvent * _pe ) override; @@ -66,7 +65,6 @@ public slots: QColor m_holdColor; int activeNotes; - void signalUpdate(); QColor fadeToColor(QColor, QColor, QTime, float); } ; diff --git a/include/LinkedModelGroupViews.h b/include/LinkedModelGroupViews.h new file mode 100644 index 00000000000..79fab76afd5 --- /dev/null +++ b/include/LinkedModelGroupViews.h @@ -0,0 +1,105 @@ +/* + * LinkedModelGroupViews.h - view for groups of linkable models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LINKEDMODELGROUPVIEWS_H +#define LINKEDMODELGROUPVIEWS_H + + +#include +#include +#include +#include + + +/** + @file LinkedModelGroupViews.h + See Lv2ViewBase.h for example usage +*/ + + +/** + View for a representative processor + + Features: + * Remove button for removable models + * Simple handling of adding, removing and model changing + + @note Neither this class, nor any inheriting classes, shall inherit + ModelView. The "view" in the name is just for consistency + with LinkedModelGroupsView. +*/ +class LinkedModelGroupView : public QWidget +{ +public: + /** + @param colNum numbers of columns for the controls + (link LEDs not counted) + */ + LinkedModelGroupView(QWidget *parent, class LinkedModelGroup* model, + std::size_t colNum); + ~LinkedModelGroupView(); + + //! Reconnect models if model changed + void modelChanged(class LinkedModelGroup *linkedModelGroup); + +protected: + //! Add a control to this widget + //! @warning This widget will own this control, do not free it + void addControl(class Control *ctrl, const std::string &id, + const std::string& display, bool removable); + + void removeControl(const QString &key); + +private: + class LinkedModelGroup* m_model; + + //! column number in surrounding grid in LinkedModelGroupsView + std::size_t m_colNum; + class ControlLayout* m_layout; + std::map> m_widgets; +}; + + +/** + Container class for one LinkedModelGroupView + + @note It's intended this class does not inherit from ModelView. + Inheriting classes need to do that, see e.g. Lv2Instrument.h +*/ +class LinkedModelGroupsView +{ +protected: + ~LinkedModelGroupsView() = default; + + //! Reconnect models if model changed; to be called by child virtuals + void modelChanged(class LinkedModelGroups* ctrlBase); + +private: + //! The base class must return the adressed group view, + //! which has the same value as "this" + virtual LinkedModelGroupView* getGroupView() = 0; +}; + + +#endif // LINKEDMODELGROUPVIEWS_H diff --git a/include/LinkedModelGroups.h b/include/LinkedModelGroups.h new file mode 100644 index 00000000000..355290d9aee --- /dev/null +++ b/include/LinkedModelGroups.h @@ -0,0 +1,175 @@ +/* + * LinkedModelGroups.h - base classes for groups of linked models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LINKEDMODELGROUPS_H +#define LINKEDMODELGROUPS_H + + +#include +#include +#include + +#include "Model.h" + + +/** + @file LinkedModelGroups.h + See Lv2ControlBase.h and Lv2Proc.h for example usage +*/ + + +/** + Base class for a group of linked models + + See the LinkedModelGroup class for explenations + + Features: + * Models are stored by their QObject::objectName + * Models are linked automatically +*/ +class LinkedModelGroup : public Model +{ + Q_OBJECT +public: + /* + Initialization + */ + //! @param parent model of the LinkedModelGroups class + LinkedModelGroup(Model* parent) : Model(parent) {} + + /* + Linking (initially only) + */ + void linkControls(LinkedModelGroup *other); + + /* + Models + */ + struct ModelInfo + { + QString m_name; + class AutomatableModel* m_model; + ModelInfo() { /* hopefully no one will use this */ } // TODO: remove? + ModelInfo(const QString& name, AutomatableModel* model) + : m_name(name), m_model(model) {} + }; + + // TODO: refactor those 2 + template + void foreach_model(const Functor& ftor) + { + for (auto itr = m_models.begin(); itr != m_models.end(); ++itr) + { + ftor(itr->first, itr->second); + } + } + + template + void foreach_model(const Functor& ftor) const + { + for (auto itr = m_models.cbegin(); itr != m_models.cend(); ++itr) + { + ftor(itr->first, itr->second); + } + } + + std::size_t modelNum() const { return m_models.size(); } + bool containsModel(const QString& name) const; + void removeControl(AutomatableModel *); + + /* + Load/Save + */ + void saveValues(class QDomDocument& doc, class QDomElement& that); + void loadValues(const class QDomElement& that); + +signals: + // NOTE: when separating core from UI, this will need to be removed + // (who would kno if the client is Qt, i.e. it may not have slots at all) + // In this case you'd e.g. send the UI something like + // "/added " + void modelAdded(AutomatableModel* added); + void modelRemoved(AutomatableModel* removed); + +public: + AutomatableModel* getModel(const std::string& s) + { + auto itr = m_models.find(s); + return (itr == m_models.end()) ? nullptr : itr->second.m_model; + } + + //! Register a further model + void addModel(class AutomatableModel* model, const QString& name); + //! Unregister a model, return true if a model was erased + bool eraseModel(const QString& name); + + //! Remove all models + void clearModels(); + +private: + //! models for the controls + std::map m_models; +}; + + +/** + Container for a group of linked models + + Each group contains the same models and model types. The models are + numbered, and equal numbered models are associated and always linked. + + A typical application are two mono plugins making a stereo plugin. + + @note Though this class can contain multiple model groups, a corresponding + view ("LinkedModelGroupViews") will only display one group, as they all have + the same values + + @note Though called "container", this class does not contain, but only + know the single groups. The inheriting classes are responsible for storage. +*/ +class LinkedModelGroups +{ +public: + virtual ~LinkedModelGroups(); + + void linkAllModels(); + + /* + Load/Save + */ + void saveSettings(class QDomDocument& doc, class QDomElement& that); + void loadSettings(const class QDomElement& that); + + /* + General + */ + //! Derived classes must return the group with index @p idx, + //! or nullptr if @p is out of range + virtual LinkedModelGroup* getGroup(std::size_t idx) = 0; + //! @see getGroup + virtual const LinkedModelGroup* getGroup(std::size_t idx) const = 0; +}; + + +#endif // LINKEDMODELGROUPS_H diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h new file mode 100644 index 00000000000..d313fd72288 --- /dev/null +++ b/include/LocklessRingBuffer.h @@ -0,0 +1,133 @@ +/* + * LocklessRingBuffer.h - LMMS wrapper for a lockless ringbuffer library + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LOCKLESSRINGBUFFER_H +#define LOCKLESSRINGBUFFER_H + +#include +#include + +#include "lmms_basics.h" +#include "lmms_export.h" +#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" + + +//! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library. +template +class LocklessRingBufferBase +{ + template + friend class LocklessRingBufferReader; +public: + LocklessRingBufferBase(std::size_t sz) : m_buffer(sz) + { + m_buffer.touch(); // reserve storage space before realtime operation starts + } + ~LocklessRingBufferBase() {}; + + std::size_t capacity() const {return m_buffer.maximum_eventual_write_space();} + std::size_t free() const {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} + +protected: + ringbuffer_t m_buffer; + QWaitCondition m_notifier; +}; + + +// The SampleFrameCopier is required because sampleFrame is just a two-element +// array and therefore does not have a copy constructor needed by std::copy. +class SampleFrameCopier +{ + const sampleFrame* m_src; +public: + SampleFrameCopier(const sampleFrame* src) : m_src(src) {} + void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest) + { + for (std::size_t i = src_offset; i < src_offset + count; i++, dest++) + { + (*dest)[0] = m_src[i][0]; + (*dest)[1] = m_src[i][1]; + } + } +}; + + +//! Standard ring buffer template for data types with copy constructor. +template +class LocklessRingBuffer : public LocklessRingBufferBase +{ +public: + LocklessRingBuffer(std::size_t sz) : LocklessRingBufferBase(sz) {}; + + std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) + { + std::size_t written = LocklessRingBufferBase::m_buffer.write(src, cnt); + // Let all waiting readers know new data are available. + if (notify) {LocklessRingBufferBase::m_notifier.wakeAll();} + return written; + } +}; + + +//! Specialized ring buffer template with write function modified to support sampleFrame. +template <> +class LocklessRingBuffer : public LocklessRingBufferBase +{ +public: + LocklessRingBuffer(std::size_t sz) : LocklessRingBufferBase(sz) {}; + + std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) + { + SampleFrameCopier copier(src); + std::size_t written = LocklessRingBufferBase::m_buffer.write_func(copier, cnt); + // Let all waiting readers know new data are available. + if (notify) {LocklessRingBufferBase::m_notifier.wakeAll();} + return written; + } +}; + + +//! Wrapper for lockless ringbuffer reader +template +class LocklessRingBufferReader : public ringbuffer_reader_t +{ +public: + LocklessRingBufferReader(LocklessRingBuffer &rb) : + ringbuffer_reader_t(rb.m_buffer), + m_notifier(&rb.m_notifier) {}; + + bool empty() const {return !this->read_space();} + void waitForData() + { + QMutex useless_lock; + useless_lock.lock(); + m_notifier->wait(&useless_lock); + useless_lock.unlock(); + } +private: + QWaitCondition *m_notifier; +}; + +#endif //LOCKLESSRINGBUFFER_H diff --git a/include/Pitch.h b/include/Pitch.h index 8a9b371b48c..2f866a1c594 100644 --- a/include/Pitch.h +++ b/include/Pitch.h @@ -25,6 +25,8 @@ #ifndef PITCH_H #define PITCH_H +#include + typedef int16_t pitch_t; const pitch_t CentsPerSemitone = 100; diff --git a/include/RingBuffer.h b/include/RingBuffer.h index c761616bd78..c7e91bd3392 100644 --- a/include/RingBuffer.h +++ b/include/RingBuffer.h @@ -32,6 +32,8 @@ #include "lmms_math.h" #include "MemoryManager.h" +/** \brief A basic LMMS ring buffer for single-thread use. For thread and realtime safe alternative see LocklessRingBuffer. +*/ class LMMS_EXPORT RingBuffer : public QObject { Q_OBJECT diff --git a/include/fft_helpers.h b/include/fft_helpers.h index 876510f8bbf..88183fb1969 100644 --- a/include/fft_helpers.h +++ b/include/fft_helpers.h @@ -103,16 +103,4 @@ int LMMS_EXPORT absspec(const fftwf_complex *complex_buffer, float *absspec_buff int LMMS_EXPORT compressbands(const float * _absspec_buffer, float * _compressedband, int _num_old, int _num_new, int _bottom, int _top); - -int LMMS_EXPORT calc13octaveband31(float * _absspec_buffer, float * _subbands, - int _num_spec, float _max_frequency); - - -/** Compute power of finite time sequence. - * Take care num_values is length of timesignal[]. - * - * @return power on success, else -1 - */ -float LMMS_EXPORT signalpower(const float *timesignal, int num_values); - #endif diff --git a/include/lmms_constants.h b/include/lmms_constants.h index befa789dd5c..ae6d3d277b1 100644 --- a/include/lmms_constants.h +++ b/include/lmms_constants.h @@ -49,4 +49,47 @@ const float F_PI_SQR = (float) LD_PI_SQR; const float F_E = (float) LD_E; const float F_E_R = (float) LD_E_R; +// Frequency ranges (in Hz). +// Arbitrary low limit for logarithmic frequency scale; >1 Hz. +const int LOWEST_LOG_FREQ = 10; + +// Full range is defined by LOWEST_LOG_FREQ and current sample rate. +enum FREQUENCY_RANGES +{ + FRANGE_FULL = 0, + FRANGE_AUDIBLE, + FRANGE_BASS, + FRANGE_MIDS, + FRANGE_HIGH +}; + +const int FRANGE_AUDIBLE_START = 20; +const int FRANGE_AUDIBLE_END = 20000; +const int FRANGE_BASS_START = 20; +const int FRANGE_BASS_END = 300; +const int FRANGE_MIDS_START = 200; +const int FRANGE_MIDS_END = 5000; +const int FRANGE_HIGH_START = 4000; +const int FRANGE_HIGH_END = 20000; + +// Amplitude ranges (in dBFS). +// Reference: full scale sine wave (-1.0 to 1.0) is 0 dB. +// Doubling or halving the amplitude produces 3 dB difference. +enum AMPLITUDE_RANGES +{ + ARANGE_EXTENDED = 0, + ARANGE_AUDIBLE, + ARANGE_LOUD, + ARANGE_SILENT +}; + +const int ARANGE_EXTENDED_START = -80; +const int ARANGE_EXTENDED_END = 20; +const int ARANGE_AUDIBLE_START = -50; +const int ARANGE_AUDIBLE_END = 0; +const int ARANGE_LOUD_START = -30; +const int ARANGE_LOUD_END = 0; +const int ARANGE_SILENT_START = -60; +const int ARANGE_SILENT_END = -10; + #endif diff --git a/include/panning.h b/include/panning.h index 8994df9ab6c..c043adf5c65 100644 --- a/include/panning.h +++ b/include/panning.h @@ -29,6 +29,7 @@ #include "lmms_basics.h" #include "panning_constants.h" #include "Midi.h" +#include "volume.h" inline stereoVolumeVector panningToVolumeVector( panning_t _p, float _scale = 1.0f ) diff --git a/include/stdshims.h b/include/stdshims.h index 85c4f457aab..5eee6543cac 100644 --- a/include/stdshims.h +++ b/include/stdshims.h @@ -21,6 +21,13 @@ std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } + +//! Overload for the case a deleter should be specified +template +std::unique_ptr make_unique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} #endif #endif // include guard diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index 9c3fe0814ca..656d18bd4d6 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -27,7 +27,13 @@ #include "Analyzer.h" +#ifdef SA_DEBUG + #include + #include +#endif + #include "embed.h" +#include "lmms_basics.h" #include "plugin_export.h" @@ -38,7 +44,7 @@ extern "C" { "Spectrum Analyzer", QT_TRANSLATE_NOOP("pluginBrowser", "A graphical spectrum analyzer."), "Martin Pavelek ", - 0x0100, + 0x0112, Plugin::Effect, new PluginPixmapLoader("logo"), NULL, @@ -50,17 +56,54 @@ extern "C" { Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) : Effect(&analyzer_plugin_descriptor, parent, key), m_processor(&m_controls), - m_controls(this) + m_controls(this), + m_processorThread(m_processor, m_inputBuffer), + // Buffer is sized to cover 4* the current maximum LMMS audio buffer size, + // so that it has some reserve space in case data processor is busy. + m_inputBuffer(4 * m_maxBufferSize) { + m_processorThread.start(); } +Analyzer::~Analyzer() +{ + m_processor.terminate(); + m_inputBuffer.wakeAll(); + m_processorThread.wait(); +} + // Take audio data and pass them to the spectrum processor. -// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) { + // Measure time spent in audio thread; both average and peak should be well under 1 ms. + #ifdef SA_DEBUG + unsigned int audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if (audio_time - m_last_dump_time > 5000000000) // print every 5 seconds + { + std::cout << "Analyzer audio thread: " << m_sum_execution / m_dump_count << " ms avg / " + << m_max_execution << " ms peak." << std::endl; + m_last_dump_time = audio_time; + m_sum_execution = m_max_execution = m_dump_count = 0; + } + #endif + if (!isEnabled() || !isRunning ()) {return false;} - if (m_controls.isViewVisible()) {m_processor.analyse(buffer, frame_count);} + + // Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. + if (m_controls.isViewVisible()) + { + // To avoid processing spikes on audio thread, data are stored in + // a lockless ringbuffer and processed in a separate thread. + m_inputBuffer.write(buffer, frame_count, true); + } + #ifdef SA_DEBUG + audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - audio_time; + m_dump_count++; + m_sum_execution += audio_time / 1000000.0; + if (audio_time / 1000000.0 > m_max_execution) {m_max_execution = audio_time / 1000000.0;} + #endif + return isRunning(); } diff --git a/plugins/SpectrumAnalyzer/Analyzer.h b/plugins/SpectrumAnalyzer/Analyzer.h index 157cc1eae20..304777c9a09 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -27,7 +27,11 @@ #ifndef ANALYZER_H #define ANALYZER_H +#include + +#include "DataprocLauncher.h" #include "Effect.h" +#include "LocklessRingBuffer.h" #include "SaControls.h" #include "SaProcessor.h" @@ -37,7 +41,7 @@ class Analyzer : public Effect { public: Analyzer(Model *parent, const Descriptor::SubPluginFeatures::Key *key); - virtual ~Analyzer() {}; + virtual ~Analyzer(); bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override; EffectControls *controls() override {return &m_controls;} @@ -47,6 +51,24 @@ class Analyzer : public Effect private: SaProcessor m_processor; SaControls m_controls; + + // Maximum LMMS buffer size (hard coded, the actual constant is hard to get) + const unsigned int m_maxBufferSize = 4096; + + // QThread::create() workaround + // Replace DataprocLauncher by QThread and replace initializer in constructor + // with the following commented line when LMMS CI starts using Qt > 5.9 + //m_processorThread = QThread::create([=]{m_processor.analyze(m_inputBuffer);}); + DataprocLauncher m_processorThread; + + LocklessRingBuffer m_inputBuffer; + + #ifdef SA_DEBUG + int m_last_dump_time; + int m_dump_count; + float m_sum_execution; + float m_max_execution; + #endif }; #endif // ANALYZER_H diff --git a/plugins/SpectrumAnalyzer/CMakeLists.txt b/plugins/SpectrumAnalyzer/CMakeLists.txt index 630fbf1be01..488495a9e3d 100644 --- a/plugins/SpectrumAnalyzer/CMakeLists.txt +++ b/plugins/SpectrumAnalyzer/CMakeLists.txt @@ -1,5 +1,7 @@ INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) + LINK_LIBRARIES(${FFTW3F_LIBRARIES}) + BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp -MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h EMBEDDED_RESOURCES *.svg logo.png) +MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h DataprocLauncher.h EMBEDDED_RESOURCES *.svg logo.png) diff --git a/plugins/SpectrumAnalyzer/DataprocLauncher.h b/plugins/SpectrumAnalyzer/DataprocLauncher.h new file mode 100644 index 00000000000..d91e0bedfcc --- /dev/null +++ b/plugins/SpectrumAnalyzer/DataprocLauncher.h @@ -0,0 +1,52 @@ +/* + * DataprocLauncher.h - QThread::create workaround for older Qt version + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef DATAPROCLAUNCHER_H +#define DATAPROCLAUNCHER_H + +#include + +#include "SaProcessor.h" +#include "LocklessRingBuffer.h" + +class DataprocLauncher : public QThread +{ +public: + explicit DataprocLauncher(SaProcessor &proc, LocklessRingBuffer &buffer) + : m_processor(&proc), + m_inputBuffer(&buffer) + { + } + +private: + void run() override + { + m_processor->analyze(*m_inputBuffer); + } + + SaProcessor *m_processor; + LocklessRingBuffer *m_inputBuffer; +}; + +#endif // DATAPROCLAUNCHER_H diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 3d3506d6540..473083da81e 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -4,13 +4,41 @@ This plugin consists of three widgets and back-end code to provide them with required data. -The top-level widget is SaControlDialog. It populates a configuration widget (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions). +The top-level widget is `SaControlDialog`. It populates configuration widgets (created dynamically) and instantiates spectrum display widgets. Its main back-end class is `SaControls`, which holds all configuration values. -SaSpectrumDisplay and SaWaterfallDisplay show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS. +`SaSpectrumView` and `SaWaterfallView` widgets show the result of spectrum analysis. Their main back-end class is `SaProcessor`, which performs FFT analysis on data received from the `Analyzer` class, which in turn handles the interface with LMMS. +## Threads + +The Spectrum Analyzer is involved in three different threads: + - **Effect mixer thread**: periodically calls `Analyzer::processAudioBuffer()` to provide the plugin with more data. This thread is real-time sensitive -- any latency spikes can potentially cause interruptions in the audio stream. For this reason, `Analyzer::processAudioBuffer()` must finish as fast as possible and must not call any functions that could cause it to be delayed for unpredictable amount of time. A lock-less ring buffer is used to safely feed data to the FFT analysis thread without risking any latency spikes due to a shared mutex being unavailable at the time of writing. + - **FFT analysis thread**: a standalone thread formed by the `SaProcessor::analyze()` function. Takes in data from the ring buffer, performs FFT analysis and prepares results for display. This thread is not real-time sensitive but excessive locking is discouraged to maintain good performance. + - **GUI thread**: periodically triggers `paintEvent()` of all Qt widgets, including `SaSpectrumView` and `SaWaterfallView`. While it is not as sensitive to latency spikes as the effect mixer thread, the `paintEvent()`s appear to be called sequentially and the execution time of each widget therefore adds to the total time needed to complete one full refresh cycle. This means the maximum frame rate of the Qt GUI will be limited to `1 / total_execution_time`. Good performance of the `paintEvent()` functions should be therefore kept in mind. -## Changelog +## Changelog + 1.1.2 2019-11-18 + - waterfall is no longer cut short when width limit is reached + - various small tweaks based on final review + 1.1.1 2019-10-13 + - improved interface for accessing SaProcessor private data + - readme file update + - other small improvements based on reviews + 1.1.0 2019-08-29 + - advanced config: expose hidden constants to user + - advanced config: add support for FFT window overlapping + - waterfall: display at native resolution on high-DPI screens + - waterfall: add cursor and improve label density + - FFT: fix normalization so that 0 dBFS matches full-scale sinewave + - FFT: decouple data acquisition from processing and display + - FFT: separate lock for reallocation (to avoid some needless waiting) + - moved ranges and other constants to a separate file + - debug: better performance measurements + - various performance optimizations + 1.0.3 2019-07-25 + - rename and tweak amplitude ranges based on feedback + 1.0.2 2019-07-12 + - variety of small changes based on code review 1.0.1 2019-06-02 - code style changes - added tool-tips diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp index 5691c0ae44a..6be298e27e4 100644 --- a/plugins/SpectrumAnalyzer/SaControls.cpp +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -50,7 +50,17 @@ SaControls::SaControls(Analyzer *effect) : m_freqRangeModel(this, tr("Frequency range")), m_ampRangeModel(this, tr("Amplitude range")), m_blockSizeModel(this, tr("FFT block size")), - m_windowModel(this, tr("FFT window type")) + m_windowModel(this, tr("FFT window type")), + + // Advanced settings knobs + m_envelopeResolutionModel(0.25f, 0.1f, 3.0f, 0.05f, this, tr("Peak envelope resolution")), + m_spectrumResolutionModel(1.5f, 0.1f, 3.0f, 0.05f, this, tr("Spectrum display resolution")), + m_peakDecayFactorModel(0.992f, 0.95f, 0.999f, 0.001f, this, tr("Peak decay multiplier")), + m_averagingWeightModel(0.15f, 0.01f, 0.5f, 0.01f, this, tr("Averaging weight")), + m_waterfallHeightModel(300.0f, 50.0f, 1000.0f, 50.0f, this, tr("Waterfall history size")), + m_waterfallGammaModel(0.30f, 0.10f, 1.00f, 0.05f, this, tr("Waterfall gamma correction")), + m_windowOverlapModel(2.0f, 1.0f, 4.0f, 1.0f, this, tr("FFT window overlap")), + m_zeroPaddingModel(2.0f, 0.0f, 4.0f, 1.0f, this, tr("FFT zero padding")) { // Frequency and amplitude ranges; order must match // FREQUENCY_RANGES and AMPLITUDE_RANGES defined in SaControls.h @@ -62,10 +72,10 @@ SaControls::SaControls(Analyzer *effect) : m_freqRangeModel.setValue(m_freqRangeModel.findText(tr("Full (auto)"))); m_ampRangeModel.addItem(tr("Extended")); - m_ampRangeModel.addItem(tr("Default")); m_ampRangeModel.addItem(tr("Audible")); - m_ampRangeModel.addItem(tr("Noise")); - m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Default"))); + m_ampRangeModel.addItem(tr("Loud")); + m_ampRangeModel.addItem(tr("Silent")); + m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Audible"))); // FFT block size labels are generated automatically, based on // FFT_BLOCK_SIZES vector defined in fft_helpers.h @@ -95,12 +105,15 @@ SaControls::SaControls(Analyzer *effect) : // Colors // Background color is defined by Qt / theme. - // Make sure the sum of colors for L and R channel stays lower or equal - // to 255. Otherwise the Waterfall pixels may overflow back to 0 even when - // the input signal isn't clipping (over 1.0). + // Make sure the sum of colors for L and R channel results into a neutral + // color that has at least one component equal to 255 (i.e. ideally white). + // This means the color overflows to zero exactly when signal reaches + // clipping threshold, indicating the problematic frequency to user. + // Mono waterfall color should have similarly at least one component at 255. m_colorL = QColor(51, 148, 204, 135); m_colorR = QColor(204, 107, 51, 135); m_colorMono = QColor(51, 148, 204, 204); + m_colorMonoW = QColor(64, 185, 255, 255); m_colorBG = QColor(7, 7, 7, 255); // ~20 % gray (after gamma correction) m_colorGrid = QColor(30, 34, 38, 255); // ~40 % gray (slightly cold / blue) m_colorLabels = QColor(192, 202, 212, 255); // ~90 % gray (slightly cold / blue) @@ -126,6 +139,15 @@ void SaControls::loadSettings(const QDomElement &_this) m_ampRangeModel.loadSettings(_this, "RangeY"); m_blockSizeModel.loadSettings(_this, "BlockSize"); m_windowModel.loadSettings(_this, "WindowType"); + + m_envelopeResolutionModel.loadSettings(_this, "EnvelopeRes"); + m_spectrumResolutionModel.loadSettings(_this, "SpectrumRes"); + m_peakDecayFactorModel.loadSettings(_this, "PeakDecayFactor"); + m_averagingWeightModel.loadSettings(_this, "AverageWeight"); + m_waterfallHeightModel.loadSettings(_this, "WaterfallHeight"); + m_waterfallGammaModel.loadSettings(_this, "WaterfallGamma"); + m_windowOverlapModel.loadSettings(_this, "WindowOverlap"); + m_zeroPaddingModel.loadSettings(_this, "ZeroPadding"); } @@ -141,4 +163,14 @@ void SaControls::saveSettings(QDomDocument &doc, QDomElement &parent) m_ampRangeModel.saveSettings(doc, parent, "RangeY"); m_blockSizeModel.saveSettings(doc, parent, "BlockSize"); m_windowModel.saveSettings(doc, parent, "WindowType"); + + m_envelopeResolutionModel.saveSettings(doc, parent, "EnvelopeRes"); + m_spectrumResolutionModel.saveSettings(doc, parent, "SpectrumRes"); + m_peakDecayFactorModel.saveSettings(doc, parent, "PeakDecayFactor"); + m_averagingWeightModel.saveSettings(doc, parent, "AverageWeight"); + m_waterfallHeightModel.saveSettings(doc, parent, "WaterfallHeight"); + m_waterfallGammaModel.saveSettings(doc, parent, "WaterfallGamma"); + m_windowOverlapModel.saveSettings(doc, parent, "WindowOverlap"); + m_zeroPaddingModel.saveSettings(doc, parent, "ZeroPadding"); + } diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h index e0b54e6a2ba..ee8a9e001fe 100644 --- a/plugins/SpectrumAnalyzer/SaControls.h +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -27,52 +27,10 @@ #include "ComboBoxModel.h" #include "EffectControls.h" +#include "lmms_constants.h" //#define SA_DEBUG 1 // define SA_DEBUG to enable performance measurements -// Frequency ranges (in Hz). -// Full range is defined by LOWEST_LOG_FREQ and current sample rate. -const int LOWEST_LOG_FREQ = 10; // arbitrary low limit for log. scale, >1 - -enum FREQUENCY_RANGES -{ - FRANGE_FULL = 0, - FRANGE_AUDIBLE, - FRANGE_BASS, - FRANGE_MIDS, - FRANGE_HIGH -}; - -const int FRANGE_AUDIBLE_START = 20; -const int FRANGE_AUDIBLE_END = 20000; -const int FRANGE_BASS_START = 20; -const int FRANGE_BASS_END = 300; -const int FRANGE_MIDS_START = 200; -const int FRANGE_MIDS_END = 5000; -const int FRANGE_HIGH_START = 4000; -const int FRANGE_HIGH_END = 20000; - -// Amplitude ranges. -// Reference: sine wave from -1.0 to 1.0 = 0 dB. -// I.e. if master volume is 100 %, positive values signify clipping. -// Doubling or halving the amplitude produces 3 dB difference. -enum AMPLITUDE_RANGES -{ - ARANGE_EXTENDED = 0, - ARANGE_DEFAULT, - ARANGE_AUDIBLE, - ARANGE_NOISE -}; - -const int ARANGE_EXTENDED_START = -80; -const int ARANGE_EXTENDED_END = 20; -const int ARANGE_DEFAULT_START = -30; -const int ARANGE_DEFAULT_END = 0; -const int ARANGE_AUDIBLE_START = -50; -const int ARANGE_AUDIBLE_END = 10; -const int ARANGE_NOISE_START = -60; -const int ARANGE_NOISE_END = -20; - class Analyzer; @@ -90,11 +48,12 @@ class SaControls : public EffectControls void loadSettings (const QDomElement &_this) override; QString nodeName() const override {return "Analyzer";} - int controlCount() override {return 12;} + int controlCount() override {return 20;} private: Analyzer *m_effect; + // basic settings BoolModel m_pauseModel; BoolModel m_refFreezeModel; @@ -111,12 +70,24 @@ class SaControls : public EffectControls ComboBoxModel m_blockSizeModel; ComboBoxModel m_windowModel; - QColor m_colorL; - QColor m_colorR; - QColor m_colorMono; - QColor m_colorBG; - QColor m_colorGrid; - QColor m_colorLabels; + // advanced settings + FloatModel m_envelopeResolutionModel; + FloatModel m_spectrumResolutionModel; + FloatModel m_peakDecayFactorModel; + FloatModel m_averagingWeightModel; + FloatModel m_waterfallHeightModel; + FloatModel m_waterfallGammaModel; + FloatModel m_windowOverlapModel; + FloatModel m_zeroPaddingModel; + + // colors (hard-coded, values must add up to specific numbers) + QColor m_colorL; //!< color of the left channel + QColor m_colorR; //!< color of the right channel + QColor m_colorMono; //!< mono color for spectrum display + QColor m_colorMonoW; //!< mono color for waterfall display + QColor m_colorBG; //!< spectrum display background color + QColor m_colorGrid; //!< color of grid lines + QColor m_colorLabels; //!< color of axis labels friend class SaControlsDialog; friend class SaSpectrumView; diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index 4ba307a4def..f1aad2a01b5 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -34,6 +34,7 @@ #include "ComboBoxModel.h" #include "embed.h" #include "Engine.h" +#include "Knob.h" #include "LedCheckbox.h" #include "PixmapButton.h" #include "SaControls.h" @@ -53,13 +54,24 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) master_layout->setContentsMargins(2, 6, 2, 8); setLayout(master_layout); - // QSplitter top: configuration section + // Display splitter top: controls section + QWidget *controls_widget = new QWidget; + QHBoxLayout *controls_layout = new QHBoxLayout; + controls_layout->setContentsMargins(0, 0, 0, 0); + controls_widget->setLayout(controls_layout); + controls_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + controls_widget->setMaximumHeight(m_configHeight); + display_splitter->addWidget(controls_widget); + + + // Basic configuration QWidget *config_widget = new QWidget; QGridLayout *config_layout = new QGridLayout; config_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); config_widget->setMaximumHeight(m_configHeight); config_widget->setLayout(config_layout); - display_splitter->addWidget(config_widget); + controls_layout->addWidget(config_widget); + controls_layout->setStretchFactor(config_widget, 10); // Pre-compute target pixmap size based on monitor DPI. // Using setDevicePixelRatio() on pixmap allows the SVG image to be razor @@ -67,6 +79,8 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // enlarged. No idea how to make Qt do it in a more reasonable way. QSize iconSize = QSize(22.0 * devicePixelRatio(), 22.0 * devicePixelRatio()); QSize buttonSize = 1.2 * iconSize; + QSize advButtonSize = QSize((m_configHeight * devicePixelRatio()) / 3, m_configHeight * devicePixelRatio()); + // pause and freeze buttons PixmapButton *pauseButton = new PixmapButton(this, tr("Pause")); @@ -79,7 +93,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) pauseButton->setInactiveGraphic(*pauseOffPixmap); pauseButton->setCheckable(true); pauseButton->setModel(&controls->m_pauseModel); - config_layout->addWidget(pauseButton, 0, 0, 2, 1); + config_layout->addWidget(pauseButton, 0, 0, 2, 1, Qt::AlignHCenter); PixmapButton *refFreezeButton = new PixmapButton(this, tr("Reference freeze")); refFreezeButton->setToolTip(tr("Freeze current input as a reference / disable falloff in peak-hold mode.")); @@ -91,7 +105,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) refFreezeButton->setInactiveGraphic(*freezeOffPixmap); refFreezeButton->setCheckable(true); refFreezeButton->setModel(&controls->m_refFreezeModel); - config_layout->addWidget(refFreezeButton, 2, 0, 2, 1); + config_layout->addWidget(refFreezeButton, 2, 0, 2, 1, Qt::AlignHCenter); // misc configuration switches LedCheckBox *waterfallButton = new LedCheckBox(tr("Waterfall"), this); @@ -169,7 +183,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) blockSizeLabel->setPixmap(blockSizeIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); config_layout->addWidget(blockSizeLabel, 0, 4, 2, 1, Qt::AlignRight); - ComboBox *blockSizeCombo = new ComboBox(this, tr("FFT block bize")); + ComboBox *blockSizeCombo = new ComboBox(this, tr("FFT block size")); blockSizeCombo->setToolTip(tr("FFT block size")); blockSizeCombo->setMinimumSize(100, 22); blockSizeCombo->setMaximumSize(200, 22); @@ -194,6 +208,117 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) processor->rebuildWindow(); connect(&controls->m_windowModel, &ComboBoxModel::dataChanged, [=] {processor->rebuildWindow();}); + // set stretch factors so that combo boxes expand first + config_layout->setColumnStretch(3, 2); + config_layout->setColumnStretch(5, 3); + + + // Advanced configuration + QWidget *advanced_widget = new QWidget; + QGridLayout *advanced_layout = new QGridLayout; + advanced_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + advanced_widget->setMaximumHeight(m_configHeight); + advanced_widget->setLayout(advanced_layout); + advanced_widget->hide(); + controls_layout->addWidget(advanced_widget); + controls_layout->setStretchFactor(advanced_widget, 10); + + // Peak envelope resolution + Knob *envelopeResolutionKnob = new Knob(knobSmall_17, this); + envelopeResolutionKnob->setModel(&controls->m_envelopeResolutionModel); + envelopeResolutionKnob->setLabel(tr("Envelope res.")); + envelopeResolutionKnob->setToolTip(tr("Increase envelope resolution for better details, decrease for better GUI performance.")); + envelopeResolutionKnob->setHintText(tr("Draw at most"), tr(" envelope points per pixel")); + advanced_layout->addWidget(envelopeResolutionKnob, 0, 0, 1, 1, Qt::AlignCenter); + + // Spectrum graph resolution + Knob *spectrumResolutionKnob = new Knob(knobSmall_17, this); + spectrumResolutionKnob->setModel(&controls->m_spectrumResolutionModel); + spectrumResolutionKnob->setLabel(tr("Spectrum res.")); + spectrumResolutionKnob->setToolTip(tr("Increase spectrum resolution for better details, decrease for better GUI performance.")); + spectrumResolutionKnob->setHintText(tr("Draw at most"), tr(" spectrum points per pixel")); + advanced_layout->addWidget(spectrumResolutionKnob, 1, 0, 1, 1, Qt::AlignCenter); + + // Peak falloff speed + Knob *peakDecayFactorKnob = new Knob(knobSmall_17, this); + peakDecayFactorKnob->setModel(&controls->m_peakDecayFactorModel); + peakDecayFactorKnob->setLabel(tr("Falloff factor")); + peakDecayFactorKnob->setToolTip(tr("Decrease to make peaks fall faster.")); + peakDecayFactorKnob->setHintText(tr("Multiply buffered value by"), ""); + advanced_layout->addWidget(peakDecayFactorKnob, 0, 1, 1, 1, Qt::AlignCenter); + + // Averaging weight + Knob *averagingWeightKnob = new Knob(knobSmall_17, this); + averagingWeightKnob->setModel(&controls->m_averagingWeightModel); + averagingWeightKnob->setLabel(tr("Averaging weight")); + averagingWeightKnob->setToolTip(tr("Decrease to make averaging slower and smoother.")); + averagingWeightKnob->setHintText(tr("New sample contributes"), ""); + advanced_layout->addWidget(averagingWeightKnob, 1, 1, 1, 1, Qt::AlignCenter); + + // Waterfall history size + Knob *waterfallHeightKnob = new Knob(knobSmall_17, this); + waterfallHeightKnob->setModel(&controls->m_waterfallHeightModel); + waterfallHeightKnob->setLabel(tr("Waterfall height")); + waterfallHeightKnob->setToolTip(tr("Increase to get slower scrolling, decrease to see fast transitions better. Warning: medium CPU usage.")); + waterfallHeightKnob->setHintText(tr("Keep"), tr(" lines")); + advanced_layout->addWidget(waterfallHeightKnob, 0, 2, 1, 1, Qt::AlignCenter); + processor->reallocateBuffers(); + connect(&controls->m_waterfallHeightModel, &FloatModel::dataChanged, [=] {processor->reallocateBuffers();}); + + // Waterfall gamma correction + Knob *waterfallGammaKnob = new Knob(knobSmall_17, this); + waterfallGammaKnob->setModel(&controls->m_waterfallGammaModel); + waterfallGammaKnob->setLabel(tr("Waterfall gamma")); + waterfallGammaKnob->setToolTip(tr("Decrease to see very weak signals, increase to get better contrast.")); + waterfallGammaKnob->setHintText(tr("Gamma value:"), ""); + advanced_layout->addWidget(waterfallGammaKnob, 1, 2, 1, 1, Qt::AlignCenter); + + // FFT window overlap + Knob *windowOverlapKnob = new Knob(knobSmall_17, this); + windowOverlapKnob->setModel(&controls->m_windowOverlapModel); + windowOverlapKnob->setLabel(tr("Window overlap")); + windowOverlapKnob->setToolTip(tr("Increase to prevent missing fast transitions arriving near FFT window edges. Warning: high CPU usage.")); + windowOverlapKnob->setHintText(tr("Each sample processed"), tr(" times")); + advanced_layout->addWidget(windowOverlapKnob, 0, 3, 1, 1, Qt::AlignCenter); + + // FFT zero padding + Knob *zeroPaddingKnob = new Knob(knobSmall_17, this); + zeroPaddingKnob->setModel(&controls->m_zeroPaddingModel); + zeroPaddingKnob->setLabel(tr("Zero padding")); + zeroPaddingKnob->setToolTip(tr("Increase to get smoother-looking spectrum. Warning: high CPU usage.")); + zeroPaddingKnob->setHintText(tr("Processing buffer is"), tr(" steps larger than input block")); + advanced_layout->addWidget(zeroPaddingKnob, 1, 3, 1, 1, Qt::AlignCenter); + processor->reallocateBuffers(); + connect(&controls->m_zeroPaddingModel, &FloatModel::dataChanged, [=] {processor->reallocateBuffers();}); + + + // Advanced settings button + PixmapButton *advancedButton = new PixmapButton(this, tr("Advanced settings")); + advancedButton->setToolTip(tr("Access advanced settings")); + QPixmap *advancedOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_on").scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + QPixmap *advancedOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_off").scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + advancedOnPixmap->setDevicePixelRatio(devicePixelRatio()); + advancedOffPixmap->setDevicePixelRatio(devicePixelRatio()); + advancedButton->setActiveGraphic(*advancedOnPixmap); + advancedButton->setInactiveGraphic(*advancedOffPixmap); + advancedButton->setCheckable(true); + controls_layout->addStretch(0); + controls_layout->addWidget(advancedButton); + + connect(advancedButton, &PixmapButton::toggled, [=](bool checked) + { + if (checked) + { + config_widget->hide(); + advanced_widget->show(); + } + else + { + config_widget->show(); + advanced_widget->hide(); + } + } + ); // QSplitter middle and bottom: spectrum display widgets m_spectrum = new SaSpectrumView(controls, processor, this); diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 9261658aa49..9d83f2916f0 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -26,15 +26,23 @@ #include "SaProcessor.h" #include +#ifdef SA_DEBUG + #include +#endif #include -#include +#ifdef SA_DEBUG + #include + #include +#endif #include #include "lmms_math.h" +#include "LocklessRingBuffer.h" -SaProcessor::SaProcessor(SaControls *controls) : +SaProcessor::SaProcessor(const SaControls *controls) : m_controls(controls), + m_terminate(false), m_inBlockSize(FFT_BLOCK_SIZES[0]), m_fftBlockSize(FFT_BLOCK_SIZES[0]), m_sampleRate(Engine::mixer()->processingSampleRate()), @@ -47,21 +55,23 @@ SaProcessor::SaProcessor(SaControls *controls) : m_fftWindow.resize(m_inBlockSize, 1.0); precomputeWindow(m_fftWindow.data(), m_inBlockSize, BLACKMAN_HARRIS); - m_bufferL.resize(m_fftBlockSize, 0); - m_bufferR.resize(m_fftBlockSize, 0); + m_bufferL.resize(m_inBlockSize, 0); + m_bufferR.resize(m_inBlockSize, 0); + m_filteredBufferL.resize(m_fftBlockSize, 0); + m_filteredBufferR.resize(m_fftBlockSize, 0); m_spectrumL = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex)); m_spectrumR = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex)); - m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferL.data(), m_spectrumL, FFTW_MEASURE); - m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferR.data(), m_spectrumR, FFTW_MEASURE); + m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_filteredBufferL.data(), m_spectrumL, FFTW_MEASURE); + m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_filteredBufferR.data(), m_spectrumR, FFTW_MEASURE); m_absSpectrumL.resize(binCount(), 0); m_absSpectrumR.resize(binCount(), 0); m_normSpectrumL.resize(binCount(), 0); m_normSpectrumR.resize(binCount(), 0); - m_history.resize(binCount() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); - - clear(); + m_waterfallHeight = 100; // a small safe value + m_history_work.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); + m_history.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); } @@ -79,169 +89,229 @@ SaProcessor::~SaProcessor() } -// Load a batch of data from LMMS; run FFT analysis if buffer is full enough. -void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) +// Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough. +void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) { - #ifdef SA_DEBUG - int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif - // only take in data if any view is visible and not paused - if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value()) + LocklessRingBufferReader reader(ring_buffer); + + // Processing thread loop + while (!m_terminate) { - const bool stereo = m_controls->m_stereoModel.value(); - fpp_t in_frame = 0; - while (in_frame < frame_count) + // If there is nothing to read, wait for notification from the writing side. + if (reader.empty()) {reader.waitForData();} + + // skip waterfall render if processing can't keep up with input + bool overload = ring_buffer.free() < ring_buffer.capacity() / 2; + + auto in_buffer = reader.read_max(ring_buffer.capacity() / 4); + std::size_t frame_count = in_buffer.size(); + + // Process received data only if any view is visible and not paused. + // Also, to prevent a momentary GUI freeze under high load (due to lock + // starvation), skip analysis when buffer reallocation is requested. + if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value() && !m_reallocating) { - // fill sample buffers and check for zero input - bool block_empty = true; - for (; in_frame < frame_count && m_framesFilledUp < m_inBlockSize; in_frame++, m_framesFilledUp++) + const bool stereo = m_controls->m_stereoModel.value(); + fpp_t in_frame = 0; + while (in_frame < frame_count) { - if (stereo) + // Lock data access to prevent reallocation from changing + // buffers and control variables. + QMutexLocker data_lock(&m_dataAccess); + + // Fill sample buffers and check for zero input. + bool block_empty = true; + for (; in_frame < frame_count && m_framesFilledUp < m_inBlockSize; in_frame++, m_framesFilledUp++) { - m_bufferL[m_framesFilledUp] = in_buffer[in_frame][0]; - m_bufferR[m_framesFilledUp] = in_buffer[in_frame][1]; + if (stereo) + { + m_bufferL[m_framesFilledUp] = in_buffer[in_frame][0]; + m_bufferR[m_framesFilledUp] = in_buffer[in_frame][1]; + } + else + { + m_bufferL[m_framesFilledUp] = + m_bufferR[m_framesFilledUp] = (in_buffer[in_frame][0] + in_buffer[in_frame][1]) * 0.5f; + } + if (in_buffer[in_frame][0] != 0.f || in_buffer[in_frame][1] != 0.f) + { + block_empty = false; + } } - else + + // Run analysis only if buffers contain enough data. + if (m_framesFilledUp < m_inBlockSize) {break;} + + // Print performance analysis once per 2 seconds if debug is enabled + #ifdef SA_DEBUG + unsigned int total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if (total_time - m_last_dump_time > 2000000000) + { + std::cout << "FFT analysis: " << std::fixed << std::setprecision(2) + << m_sum_execution / m_dump_count << " ms avg / " + << m_max_execution << " ms peak, executing " + << m_dump_count << " times per second (" + << m_sum_execution / 20.0 << " % CPU usage)." << std::endl; + m_last_dump_time = total_time; + m_sum_execution = m_max_execution = m_dump_count = 0; + } + #endif + + // update sample rate + m_sampleRate = Engine::mixer()->processingSampleRate(); + + // apply FFT window + for (unsigned int i = 0; i < m_inBlockSize; i++) { - m_bufferL[m_framesFilledUp] = - m_bufferR[m_framesFilledUp] = (in_buffer[in_frame][0] + in_buffer[in_frame][1]) * 0.5f; + m_filteredBufferL[i] = m_bufferL[i] * m_fftWindow[i]; + m_filteredBufferR[i] = m_bufferR[i] * m_fftWindow[i]; } - if (in_buffer[in_frame][0] != 0.f || in_buffer[in_frame][1] != 0.f) + + // Run FFT on left channel, convert the result to absolute magnitude + // spectrum and normalize it. + fftwf_execute(m_fftPlanL); + absspec(m_spectrumL, m_absSpectrumL.data(), binCount()); + normalize(m_absSpectrumL, m_normSpectrumL, m_inBlockSize); + + // repeat analysis for right channel if stereo processing is enabled + if (stereo) { - block_empty = false; + fftwf_execute(m_fftPlanR); + absspec(m_spectrumR, m_absSpectrumR.data(), binCount()); + normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize); } - } - - // Run analysis only if buffers contain enough data. - // Also, to prevent audio interruption and a momentary GUI freeze, - // skip analysis if buffers are being reallocated. - if (m_framesFilledUp < m_inBlockSize || m_reallocating) {return;} - - // update sample rate - m_sampleRate = Engine::mixer()->processingSampleRate(); - - // apply FFT window - for (unsigned int i = 0; i < m_inBlockSize; i++) - { - m_bufferL[i] = m_bufferL[i] * m_fftWindow[i]; - m_bufferR[i] = m_bufferR[i] * m_fftWindow[i]; - } - - // lock data shared with SaSpectrumView and SaWaterfallView - QMutexLocker lock(&m_dataAccess); - - // Run FFT on left channel, convert the result to absolute magnitude - // spectrum and normalize it. - fftwf_execute(m_fftPlanL); - absspec(m_spectrumL, m_absSpectrumL.data(), binCount()); - normalize(m_absSpectrumL, m_normSpectrumL, m_inBlockSize); - - // repeat analysis for right channel if stereo processing is enabled - if (stereo) - { - fftwf_execute(m_fftPlanR); - absspec(m_spectrumR, m_absSpectrumR.data(), binCount()); - normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize); - } - // count empty lines so that empty history does not have to update - if (block_empty && m_waterfallNotEmpty) - { - m_waterfallNotEmpty -= 1; - } - else if (!block_empty) - { - m_waterfallNotEmpty = m_waterfallHeight + 2; - } + // count empty lines so that empty history does not have to update + if (block_empty && m_waterfallNotEmpty) + { + m_waterfallNotEmpty -= 1; + } + else if (!block_empty) + { + m_waterfallNotEmpty = m_waterfallHeight + 2; + } - if (m_waterfallActive && m_waterfallNotEmpty) - { - // move waterfall history one line down and clear the top line - QRgb *pixel = (QRgb *)m_history.data(); - std::copy(pixel, - pixel + binCount() * m_waterfallHeight - binCount(), - pixel + binCount()); - memset(pixel, 0, binCount() * sizeof (QRgb)); - - // add newest result on top - int target; // pixel being constructed - float accL = 0; // accumulators for merging multiple bins - float accR = 0; - - for (unsigned int i = 0; i < binCount(); i++) + if (m_waterfallActive && m_waterfallNotEmpty) { - // Every frequency bin spans a frequency range that must be - // partially or fully mapped to a pixel. Any inconsistency - // may be seen in the spectrogram as dark or white lines -- - // play white noise to confirm your change did not break it. - float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, binCount()); - float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, binCount()); - if (m_controls->m_logXModel.value()) + // move waterfall history one line down and clear the top line + QRgb *pixel = (QRgb *)m_history_work.data(); + std::copy(pixel, + pixel + waterfallWidth() * m_waterfallHeight - waterfallWidth(), + pixel + waterfallWidth()); + memset(pixel, 0, waterfallWidth() * sizeof (QRgb)); + + // add newest result on top + int target; // pixel being constructed + float accL = 0; // accumulators for merging multiple bins + float accR = 0; + for (unsigned int i = 0; i < binCount(); i++) { - // Logarithmic scale - if (band_end - band_start > 1.0) + // fill line with red color to indicate lost data if CPU cannot keep up + if (overload && i < waterfallWidth()) { - // band spans multiple pixels: draw all pixels it covers - for (target = (int)band_start; target < (int)band_end; target++) - { - if (target >= 0 && target < binCount()) - { - pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); - } - } - // save remaining portion of the band for the following band / pixel - // (in case the next band uses sub-pixel drawing) - accL = (band_end - (int)band_end) * m_normSpectrumL[i]; - accR = (band_end - (int)band_end) * m_normSpectrumR[i]; + pixel[i] = qRgb(42, 0, 0); + continue; } - else + + // Every frequency bin spans a frequency range that must be + // partially or fully mapped to a pixel. Any inconsistency + // may be seen in the spectrogram as dark or white lines -- + // play white noise to confirm your change did not break it. + float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, waterfallWidth()); + float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, waterfallWidth()); + if (m_controls->m_logXModel.value()) { - // sub-pixel drawing; add contribution of current band - target = (int)band_start; - if ((int)band_start == (int)band_end) + // Logarithmic scale + if (band_end - band_start > 1.0) { - // band ends within current target pixel, accumulate - accL += (band_end - band_start) * m_normSpectrumL[i]; - accR += (band_end - band_start) * m_normSpectrumR[i]; + // band spans multiple pixels: draw all pixels it covers + for (target = (int)band_start; target < (int)band_end; target++) + { + if (target >= 0 && target < waterfallWidth()) + { + pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + } + } + // save remaining portion of the band for the following band / pixel + // (in case the next band uses sub-pixel drawing) + accL = (band_end - (int)band_end) * m_normSpectrumL[i]; + accR = (band_end - (int)band_end) * m_normSpectrumR[i]; } else { - // Band ends in the next pixel -- finalize the current pixel. - // Make sure contribution is split correctly on pixel boundary. - accL += ((int)band_end - band_start) * m_normSpectrumL[i]; - accR += ((int)band_end - band_start) * m_normSpectrumR[i]; - - if (target >= 0 && target < binCount()) {pixel[target] = makePixel(accL, accR);} + // sub-pixel drawing; add contribution of current band + target = (int)band_start; + if ((int)band_start == (int)band_end) + { + // band ends within current target pixel, accumulate + accL += (band_end - band_start) * m_normSpectrumL[i]; + accR += (band_end - band_start) * m_normSpectrumR[i]; + } + else + { + // Band ends in the next pixel -- finalize the current pixel. + // Make sure contribution is split correctly on pixel boundary. + accL += ((int)band_end - band_start) * m_normSpectrumL[i]; + accR += ((int)band_end - band_start) * m_normSpectrumR[i]; - // save remaining portion of the band for the following band / pixel - accL = (band_end - (int)band_end) * m_normSpectrumL[i]; - accR = (band_end - (int)band_end) * m_normSpectrumR[i]; + if (target >= 0 && target < waterfallWidth()) {pixel[target] = makePixel(accL, accR);} + + // save remaining portion of the band for the following band / pixel + accL = (band_end - (int)band_end) * m_normSpectrumL[i]; + accR = (band_end - (int)band_end) * m_normSpectrumR[i]; + } } } - } - else - { - // Linear: always draws one or more pixels per band - for (target = (int)band_start; target < band_end; target++) + else { - if (target >= 0 && target < binCount()) + // Linear: always draws one or more pixels per band + for (target = (int)band_start; target < band_end; target++) { - pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + if (target >= 0 && target < waterfallWidth()) + { + pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + } } } } + + // Copy work buffer to result buffer. Done only if requested, so + // that time isn't wasted on updating faster than display FPS. + // (The copy is about as expensive as the movement.) + if (m_flipRequest) + { + m_history = m_history_work; + m_flipRequest = false; + } } - } - #ifdef SA_DEBUG - // report FFT processing speed - start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time; - std::cout << "Processed " << m_framesFilledUp << " samples in " << start_time / 1000000.0 << " ms" << std::endl; - #endif - - // clean up before checking for more data from input buffer - m_framesFilledUp = 0; - } - } + // clean up before checking for more data from input buffer + const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); + if (overlaps == 1) // Discard buffer, each sample used only once + { + m_framesFilledUp = 0; + } + else + { + // Drop only a part of the buffer from the beginning, so that new + // data can be added to the end. This means the older samples will + // be analyzed again, but in a different position in the window, + // making short transient signals show up better in the waterfall. + const unsigned int drop = m_inBlockSize / overlaps; + std::move(m_bufferL.begin() + drop, m_bufferL.end(), m_bufferL.begin()); + std::move(m_bufferR.begin() + drop, m_bufferR.end(), m_bufferR.begin()); + m_framesFilledUp -= drop; + } + + #ifdef SA_DEBUG + // measure overall FFT processing speed + total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; + m_dump_count++; + m_sum_execution += total_time / 1000000.0; + if (total_time / 1000000.0 > m_max_execution) {m_max_execution = total_time / 1000000.0;} + #endif + } // frame filler and processing + } // process if active + } // thread loop end } @@ -251,8 +321,9 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) // Gamma correction is applied to make small values more visible and to make // a linear gradient actually appear roughly linear. The correction should be // around 0.42 to 0.45 for sRGB displays (or lower for bigger visibility boost). -QRgb SaProcessor::makePixel(float left, float right, float gamma_correction) const +QRgb SaProcessor::makePixel(float left, float right) const { + const float gamma_correction = m_controls->m_waterfallGammaModel.value(); if (m_controls->m_stereoModel.value()) { float ampL = pow(left, gamma_correction); @@ -265,9 +336,9 @@ QRgb SaProcessor::makePixel(float left, float right, float gamma_correction) con { float ampL = pow(left, gamma_correction); // make mono color brighter to compensate for the fact it is not summed - return qRgb(m_controls->m_colorMono.lighter().red() * ampL, - m_controls->m_colorMono.lighter().green() * ampL, - m_controls->m_colorMono.lighter().blue() * ampL); + return qRgb(m_controls->m_colorMonoW.red() * ampL, + m_controls->m_colorMonoW.green() * ampL, + m_controls->m_colorMonoW.blue() * ampL); } } @@ -301,6 +372,7 @@ void SaProcessor::reallocateBuffers() { new_in_size = FFT_BLOCK_SIZES.back(); } + m_zeroPadFactor = m_controls->m_zeroPaddingModel.value(); if (new_size_index + m_zeroPadFactor < FFT_BLOCK_SIZES.size()) { new_fft_size = FFT_BLOCK_SIZES[new_size_index + m_zeroPadFactor]; @@ -312,12 +384,16 @@ void SaProcessor::reallocateBuffers() new_bins = new_fft_size / 2 +1; - // Lock data shared with SaSpectrumView and SaWaterfallView. - // The m_reallocating is here to tell analyse() to avoid asking for the - // lock, since fftw3 can take a while to find the fastest FFT algorithm - // for given machine, which would produce interruption in the audio stream. + // Use m_reallocating to tell analyze() to avoid asking for the lock. This + // is needed because under heavy load the FFT thread requests data lock so + // often that this routine could end up waiting even for several seconds. m_reallocating = true; - QMutexLocker lock(&m_dataAccess); + + // Lock data shared with SaSpectrumView and SaWaterfallView. + // Reallocation lock must be acquired first to avoid deadlock (a view class + // may already have it and request the "stronger" data lock on top of that). + QMutexLocker reloc_lock(&m_reallocationAccess); + QMutexLocker data_lock(&m_dataAccess); // destroy old FFT plan and free the result buffer if (m_fftPlanL != NULL) {fftwf_destroy_plan(m_fftPlanL);} @@ -328,30 +404,42 @@ void SaProcessor::reallocateBuffers() // allocate new space, create new plan and resize containers m_fftWindow.resize(new_in_size, 1.0); precomputeWindow(m_fftWindow.data(), new_in_size, (FFT_WINDOWS) m_controls->m_windowModel.value()); - m_bufferL.resize(new_fft_size, 0); - m_bufferR.resize(new_fft_size, 0); + m_bufferL.resize(new_in_size, 0); + m_bufferR.resize(new_in_size, 0); + m_filteredBufferL.resize(new_fft_size, 0); + m_filteredBufferR.resize(new_fft_size, 0); m_spectrumL = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex)); m_spectrumR = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex)); - m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferL.data(), m_spectrumL, FFTW_MEASURE); - m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferR.data(), m_spectrumR, FFTW_MEASURE); + m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_filteredBufferL.data(), m_spectrumL, FFTW_MEASURE); + m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_filteredBufferR.data(), m_spectrumR, FFTW_MEASURE); if (m_fftPlanL == NULL || m_fftPlanR == NULL) { - std::cerr << "Failed to create new FFT plan!" << std::endl; + #ifdef SA_DEBUG + std::cerr << "Analyzer: failed to create new FFT plan!" << std::endl; + #endif } m_absSpectrumL.resize(new_bins, 0); m_absSpectrumR.resize(new_bins, 0); m_normSpectrumL.resize(new_bins, 0); m_normSpectrumR.resize(new_bins, 0); - m_history.resize(new_bins * m_waterfallHeight * sizeof qRgb(0,0,0), 0); + m_waterfallHeight = m_controls->m_waterfallHeightModel.value(); + m_history_work.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth) + * m_waterfallHeight + * sizeof qRgb(0,0,0), 0); + m_history.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth) + * m_waterfallHeight + * sizeof qRgb(0,0,0), 0); // done; publish new sizes and clean up m_inBlockSize = new_in_size; m_fftBlockSize = new_fft_size; - lock.unlock(); + data_lock.unlock(); + reloc_lock.unlock(); m_reallocating = false; + clear(); } @@ -369,17 +457,39 @@ void SaProcessor::rebuildWindow() // Note: may take a few milliseconds, do not call in a loop! void SaProcessor::clear() { + const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); QMutexLocker lock(&m_dataAccess); - m_framesFilledUp = 0; + // If there is any window overlap, leave space only for the new samples + // and treat the rest at initialized with zeros. Prevents missing + // transients at the start of the very first block. + m_framesFilledUp = m_inBlockSize - m_inBlockSize / overlaps; std::fill(m_bufferL.begin(), m_bufferL.end(), 0); std::fill(m_bufferR.begin(), m_bufferR.end(), 0); + std::fill(m_filteredBufferL.begin(), m_filteredBufferL.end(), 0); + std::fill(m_filteredBufferR.begin(), m_filteredBufferR.end(), 0); std::fill(m_absSpectrumL.begin(), m_absSpectrumL.end(), 0); std::fill(m_absSpectrumR.begin(), m_absSpectrumR.end(), 0); std::fill(m_normSpectrumL.begin(), m_normSpectrumL.end(), 0); std::fill(m_normSpectrumR.begin(), m_normSpectrumR.end(), 0); + std::fill(m_history_work.begin(), m_history_work.end(), 0); std::fill(m_history.begin(), m_history.end(), 0); } +// Clear only history work buffer. Used to flush old data when waterfall +// is shown after a period of inactivity. +void SaProcessor::clearHistory() +{ + QMutexLocker lock(&m_dataAccess); + std::fill(m_history_work.begin(), m_history_work.end(), 0); +} + +// Check if result buffers contain any non-zero values +bool SaProcessor::spectrumNotEmpty() +{ + QMutexLocker lock(&m_reallocationAccess); + return notEmpty(m_normSpectrumL) || notEmpty(m_normSpectrumR); +} + // -------------------------------------- // Frequency conversion helpers @@ -407,6 +517,17 @@ unsigned int SaProcessor::binCount() const } +// Return the final width of waterfall display buffer. +// Normally the waterfall width equals the number of frequency bins, but the +// FFT transform can easily produce more bins than can be reasonably useful for +// currently used display resolutions. This function limits width of the final +// image to a given size, which is then used during waterfall render and display. +unsigned int SaProcessor::waterfallWidth() const +{ + return binCount() < m_waterfallMaxWidth ? binCount() : m_waterfallMaxWidth; +} + + // Return the center frequency of given frequency bin. float SaProcessor::binToFreq(unsigned int bin_index) const { @@ -499,10 +620,10 @@ float SaProcessor::getAmpRangeMin(bool linear) const switch (m_controls->m_ampRangeModel.value()) { case ARANGE_EXTENDED: return ARANGE_EXTENDED_START; - case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START; - case ARANGE_NOISE: return ARANGE_NOISE_START; + case ARANGE_SILENT: return ARANGE_SILENT_START; + case ARANGE_LOUD: return ARANGE_LOUD_START; default: - case ARANGE_DEFAULT: return ARANGE_DEFAULT_START; + case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START; } } @@ -512,10 +633,10 @@ float SaProcessor::getAmpRangeMax() const switch (m_controls->m_ampRangeModel.value()) { case ARANGE_EXTENDED: return ARANGE_EXTENDED_END; - case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END; - case ARANGE_NOISE: return ARANGE_NOISE_END; + case ARANGE_SILENT: return ARANGE_SILENT_END; + case ARANGE_LOUD: return ARANGE_LOUD_END; default: - case ARANGE_DEFAULT: return ARANGE_DEFAULT_END; + case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END; } } diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index ae2df16f8c8..0c396b3c031 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -27,6 +27,7 @@ #ifndef SAPROCESSOR_H #define SAPROCESSOR_H +#include #include #include #include @@ -34,27 +35,45 @@ #include "fft_helpers.h" #include "SaControls.h" +template +class LocklessRingBuffer; //! Receives audio data, runs FFT analysis and stores the result. class SaProcessor { public: - explicit SaProcessor(SaControls *controls); + explicit SaProcessor(const SaControls *controls); virtual ~SaProcessor(); - void analyse(sampleFrame *in_buffer, const fpp_t frame_count); + // analysis thread and a method to terminate it + void analyze(LocklessRingBuffer &ring_buffer); + void terminate() {m_terminate = true;} // inform processor if any processing is actually required void setSpectrumActive(bool active); void setWaterfallActive(bool active); + void flipRequest() {m_flipRequest = true;} // request refresh of history buffer // configuration is taken from models in SaControls; some changes require // an exlicit update request (reallocation and window rebuild) void reallocateBuffers(); void rebuildWindow(); void clear(); + void clearHistory(); + + const float *getSpectrumL() const {return m_normSpectrumL.data();} + const float *getSpectrumR() const {return m_normSpectrumR.data();} + const uchar *getHistory() const {return m_history.data();} // information about results and unit conversion helpers + unsigned int inBlockSize() const {return m_inBlockSize;} + unsigned int binCount() const; //!< size of output (frequency domain) data block + bool spectrumNotEmpty(); //!< check if result buffers contain any non-zero values + + unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display) + unsigned int waterfallHeight() const {return m_waterfallHeight;} + bool waterfallNotEmpty() const {return m_waterfallNotEmpty;} + float binToFreq(unsigned int bin_index) const; float binBandwidth() const; @@ -72,26 +91,38 @@ class SaProcessor float getAmpRangeMin(bool linear = false) const; float getAmpRangeMax() const; - // data access lock must be acquired by any friendly class that touches - // the results, mainly to prevent unexpected mid-way reallocation + // Reallocation lock prevents the processor from changing size of its buffers. + // It is used to keep consistent bin-to-frequency mapping while drawing the + // spectrum and to make sure reading side does not find itself out of bounds. + // The processor is meanwhile free to work on another block. + QMutex m_reallocationAccess; + // Data access lock prevents the processor from changing both size and content + // of its buffers. It is used when writing to a result buffer, or when a friendly + // class reads them and needs guaranteed data consistency. + // It causes FFT analysis to be paused, so this lock should be used sparingly. + // If using both locks at the same time, reallocation lock MUST be acquired first. QMutex m_dataAccess; + private: - SaControls *m_controls; + const SaControls *m_controls; + + // thread communication and control + bool m_terminate; // currently valid configuration - const unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size - unsigned int m_inBlockSize; //!< size of input (time domain) data block + unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size + std::atomic m_inBlockSize;//!< size of input (time domain) data block unsigned int m_fftBlockSize; //!< size of padded block for FFT processing unsigned int m_sampleRate; - unsigned int binCount() const; //!< size of output (frequency domain) data block - // data buffers (roughly in the order of processing, from input to output) unsigned int m_framesFilledUp; std::vector m_bufferL; //!< time domain samples (left) std::vector m_bufferR; //!< time domain samples (right) std::vector m_fftWindow; //!< precomputed window function coefficients + std::vector m_filteredBufferL; //!< time domain samples with window function applied (left) + std::vector m_filteredBufferR; //!< time domain samples with window function applied (right) fftwf_plan m_fftPlanL; fftwf_plan m_fftPlanR; fftwf_complex *m_spectrumL; //!< frequency domain samples (complex) (left) @@ -102,21 +133,28 @@ class SaProcessor std::vector m_normSpectrumR; //!< frequency domain samples (normalized) (right) // spectrum history for waterfall: new normSpectrum lines are added on top - std::vector m_history; - const unsigned int m_waterfallHeight = 200; // Number of stored lines. - // Note: high values may make it harder to see transients. + std::vector m_history_work; //!< local history buffer for render + std::vector m_history; //!< public buffer for reading + bool m_flipRequest; //!< update public buffer only when requested + std::atomic m_waterfallHeight; //!< number of stored lines in history buffer + // Note: high values may make it harder to see transients. + const unsigned int m_waterfallMaxWidth = 3840; // book keeping bool m_spectrumActive; bool m_waterfallActive; - unsigned int m_waterfallNotEmpty; + std::atomic m_waterfallNotEmpty; //!< number of lines remaining visible on display bool m_reallocating; // merge L and R channels and apply gamma correction to make a spectrogram pixel - QRgb makePixel(float left, float right, float gamma_correction = 0.30) const; - - friend class SaSpectrumView; - friend class SaWaterfallView; + QRgb makePixel(float left, float right) const; + + #ifdef SA_DEBUG + unsigned int m_last_dump_time; + unsigned int m_dump_count; + float m_sum_execution; + float m_max_execution; + #endif }; #endif // SAPROCESSOR_H diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 746d52cfdc1..13aaeb72418 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -39,7 +39,6 @@ #ifdef SA_DEBUG #include - #include #endif @@ -68,7 +67,11 @@ SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWi m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); - m_cursor = QPoint(0, 0); + m_cursor = QPointF(0, 0); + + #ifdef SA_DEBUG + m_execution_avg = m_path_avg = m_draw_avg = 0; + #endif } @@ -134,12 +137,20 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) 2.0, 2.0); #ifdef SA_DEBUG - // display what FPS would be achieved if spectrum display ran in a loop + // display performance measurements if enabled total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; + m_execution_avg = 0.95 * m_execution_avg + 0.05 * total_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft, - QString(std::string("Max FPS: " + std::to_string(1000000000.0 / total_time)).c_str())); + painter.drawText(m_displayRight -150, 10, 130, 16, Qt::AlignLeft, + QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms")); + painter.drawText(m_displayRight -150, 30, 130, 16, Qt::AlignLeft, + QString("Buff. upd. avg: ").append(std::to_string(m_refresh_avg).substr(0, 5).c_str()).append(" ms")); + painter.drawText(m_displayRight -150, 50, 130, 16, Qt::AlignLeft, + QString("Path build avg: ").append(std::to_string(m_path_avg).substr(0, 5).c_str()).append(" ms")); + painter.drawText(m_displayRight -150, 70, 130, 16, Qt::AlignLeft, + QString("Path draw avg: ").append(std::to_string(m_draw_avg).substr(0, 5).c_str()).append(" ms")); + #endif } @@ -148,22 +159,14 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) void SaSpectrumView::drawSpectrum(QPainter &painter) { #ifdef SA_DEBUG - int path_time = 0, draw_time = 0; + int draw_time = 0; #endif // draw the graph only if there is any input, averaging residue or peaks - QMutexLocker lock(&m_processor->m_dataAccess); - if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR)) + if (m_decaySum > 0 || m_processor->spectrumNotEmpty()) { - lock.unlock(); - #ifdef SA_DEBUG - path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif // update data buffers and reconstruct paths refreshPaths(); - #ifdef SA_DEBUG - path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time; - #endif // draw stored paths #ifdef SA_DEBUG @@ -199,17 +202,10 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time; #endif } - else - { - lock.unlock(); - } #ifdef SA_DEBUG - // display measurement results - painter.drawText(m_displayRight -100, 90, 100, 16, Qt::AlignLeft, - QString(std::string("Path ms: " + std::to_string(path_time / 1000000.0)).c_str())); - painter.drawText(m_displayRight -100, 110, 100, 16, Qt::AlignLeft, - QString(std::string("Draw ms: " + std::to_string(draw_time / 1000000.0)).c_str())); + // save performance measurement result + m_draw_avg = 0.95 * m_draw_avg + 0.05 * draw_time / 1000000.0; #endif } @@ -218,9 +214,9 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) // and build QPainter paths. void SaSpectrumView::refreshPaths() { - // Lock is required for the entire function, mainly to prevent block size - // changes from causing reallocation of data structures mid-way. - QMutexLocker lock(&m_processor->m_dataAccess); + // Reallocation lock is required for the entire function, to keep display + // buffer size consistent with block size. + QMutexLocker reloc_lock(&m_processor->m_reallocationAccess); // check if bin count changed and reallocate display buffers accordingly if (m_processor->binCount() != m_displayBufferL.size()) @@ -240,8 +236,8 @@ void SaSpectrumView::refreshPaths() int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif m_decaySum = 0; - updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data()); - updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data()); + updateBuffers(m_processor->getSpectrumL(), m_displayBufferL.data(), m_peakBufferL.data()); + updateBuffers(m_processor->getSpectrumR(), m_displayBufferR.data(), m_peakBufferR.data()); #ifdef SA_DEBUG refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time; #endif @@ -254,41 +250,43 @@ void SaSpectrumView::refreshPaths() } #ifdef SA_DEBUG - int make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + int path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // Use updated display buffers to prepare new paths for QPainter. // This is the second slowest action (first is the subsequent drawing); use // the resolution parameter to balance display quality and performance. - m_pathL = makePath(m_displayBufferL, 1.5); + m_pathL = makePath(m_displayBufferL, m_controls->m_spectrumResolutionModel.value()); if (m_controls->m_stereoModel.value()) { - m_pathR = makePath(m_displayBufferR, 1.5); + m_pathR = makePath(m_displayBufferR, m_controls->m_spectrumResolutionModel.value()); } if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value()) { - m_pathPeakL = makePath(m_peakBufferL, 0.25); + m_pathPeakL = makePath(m_peakBufferL, m_controls->m_envelopeResolutionModel.value()); if (m_controls->m_stereoModel.value()) { - m_pathPeakR = makePath(m_peakBufferR, 0.25); + m_pathPeakR = makePath(m_peakBufferR, m_controls->m_envelopeResolutionModel.value()); } } #ifdef SA_DEBUG - make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time; + path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time; #endif #ifdef SA_DEBUG - // print measurement results - std::cout << "Buffer update ms: " << std::to_string(refresh_time / 1000000.0) << ", "; - std::cout << "Path-make ms: " << std::to_string(make_time / 1000000.0) << std::endl; + // save performance measurement results + m_refresh_avg = 0.95 * m_refresh_avg + 0.05 * refresh_time / 1000000.0; + m_path_avg = .95f * m_path_avg + .05f * path_time / 1000000.f; #endif } // Update display buffers: add new data, update average and peaks / reference. // Output the sum of all displayed values -- draw only if it is non-zero. -// NOTE: The calling function is responsible for acquiring SaProcessor data -// access lock! -void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer) +// NOTE: The calling function is responsible for acquiring SaProcessor +// reallocation access lock! Data access lock is not needed: the final result +// buffer is updated very quickly and the worst case is that one frame will be +// part new, part old. At reasonable frame rate, such difference is invisible.. +void SaSpectrumView::updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer) { for (int n = 0; n < m_processor->binCount(); n++) { @@ -297,7 +295,8 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float { if (m_controls->m_smoothModel.value()) { - displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_smoothFactor); + const float smoothFactor = m_controls->m_averagingWeightModel.value(); + displayBuffer[n] = spectrum[n] * smoothFactor + displayBuffer[n] * (1 - smoothFactor); } else { @@ -319,7 +318,7 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float } else if (!m_controls->m_refFreezeModel.value()) { - peakBuffer[n] = peakBuffer[n] * m_peakDecayFactor; + peakBuffer[n] = peakBuffer[n] * m_controls->m_peakDecayFactorModel.value(); } } else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value()) @@ -539,38 +538,52 @@ void SaSpectrumView::drawGrid(QPainter &painter) // Draw cursor and its coordinates if it is within display bounds. void SaSpectrumView::drawCursor(QPainter &painter) { - if( m_cursor.x() >= m_displayLeft + if ( m_cursor.x() >= m_displayLeft && m_cursor.x() <= m_displayRight && m_cursor.y() >= m_displayTop && m_cursor.y() <= m_displayBottom) { // cursor lines painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); - painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); - - // coordinates + painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom)); + painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y())); + + // coordinates: background box + QFontMetrics fontMetrics = painter.fontMetrics(); + unsigned int const box_left = 5; + unsigned int const box_top = 5; + unsigned int const box_margin = 3; + unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 HzdBFS").height() + box_margin); + unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "-99.9 dBFS").width() + 2*box_margin; painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -60, 5, 100, 16, Qt::AlignLeft, "Cursor"); + painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top, + box_width, box_height, QColor(0, 0, 0, 64)); + // coordinates: text + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); QString tmps; + // frequency int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); - tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); - painter.drawText(m_displayRight -60, 18, 100, 16, Qt::AlignLeft, tmps); + tmps = QString("%1 Hz").arg(xFreq); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_margin, + box_width, box_height / 2, Qt::AlignLeft, tmps); // amplitude float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); if (m_controls->m_logYModel.value()) { - tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dB").c_str()); + tmps = QString(std::to_string(yAmp).substr(0, 5).c_str()).append(" dBFS"); } else { // add 0.0005 to get proper rounding to 3 decimal places - tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); + tmps = QString(std::to_string(0.0005f + yAmp).substr(0, 5).c_str()); } - painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_height / 2, + box_width, box_height / 2, Qt::AlignLeft, tmps); } } @@ -774,14 +787,18 @@ void SaSpectrumView::periodicUpdate() // Handle mouse input: set new cursor position. +// For some reason (a bug?), localPos() only returns integers. As a workaround +// the fractional part is taken from windowPos() (which works correctly). void SaSpectrumView::mouseMoveEvent(QMouseEvent *event) { - m_cursor = event->pos(); + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); } void SaSpectrumView::mousePressEvent(QMouseEvent *event) { - m_cursor = event->pos(); + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); } diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.h b/plugins/SpectrumAnalyzer/SaSpectrumView.h index 0db5852e19d..b59264d9ce7 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.h +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.h @@ -27,6 +27,8 @@ #ifndef SASPECTRUMVIEW_H #define SASPECTRUMVIEW_H +#include "SaControls.h" + #include #include #include @@ -34,7 +36,6 @@ class QMouseEvent; class QPainter; -class SaControls; class SaProcessor; //! Widget that displays a spectrum curve and frequency / amplitude grid @@ -84,7 +85,7 @@ private slots: std::vector m_displayBufferR; std::vector m_peakBufferL; std::vector m_peakBufferR; - void updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer); + void updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer); // final paths to be drawn by QPainter and methods to build them QPainterPath m_pathL; @@ -99,14 +100,11 @@ private slots: bool m_freezeRequest; // new reference should be acquired bool m_frozen; // a reference is currently stored in the peakBuffer - const float m_smoothFactor = 0.15; // alpha for exponential smoothing - const float m_peakDecayFactor = 0.992; // multiplier for gradual peak decay - // top level: refresh buffers, make paths and draw the spectrum void drawSpectrum(QPainter &painter); // current cursor location and a method to draw it - QPoint m_cursor; + QPointF m_cursor; void drawCursor(QPainter &painter); // wrappers for most used SaProcessor conversion helpers @@ -121,6 +119,13 @@ private slots: unsigned int m_displayLeft; unsigned int m_displayRight; unsigned int m_displayWidth; + + #ifdef SA_DEBUG + float m_execution_avg; + float m_refresh_avg; + float m_path_avg; + float m_draw_avg; + #endif }; #endif // SASPECTRUMVIEW_H diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 617e80b2c49..e015d31ef74 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -23,8 +23,12 @@ #include "SaWaterfallView.h" #include +#ifdef SA_DEBUG + #include +#endif #include #include +#include #include #include #include @@ -47,8 +51,22 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); + m_displayTop = 1; + m_displayBottom = height() -2; + m_displayLeft = 26; + m_displayRight = width() -26; + m_displayWidth = m_displayRight - m_displayLeft; + m_displayHeight = m_displayBottom - m_displayTop; + m_timeTics = makeTimeTics(); - m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate(); + m_oldSecondsPerLine = 0; + m_oldHeight = 0; + + m_cursor = QPointF(0, 0); + + #ifdef SA_DEBUG + m_execution_avg = 0; + #endif } @@ -58,15 +76,14 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q void SaWaterfallView::paintEvent(QPaintEvent *event) { #ifdef SA_DEBUG - int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + unsigned int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif - // all drawing done here, local variables are sufficient for the boundary - const int displayTop = 1; - const int displayBottom = height() -2; - const int displayLeft = 26; - const int displayRight = width() -26; - const int displayWidth = displayRight - displayLeft; + // update boundary + m_displayBottom = height() -2; + m_displayRight = width() -26; + m_displayWidth = m_displayRight - m_displayLeft; + m_displayHeight = m_displayBottom - m_displayTop; float label_width = 20; float label_height = 16; float margin = 2; @@ -75,10 +92,11 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) painter.setRenderHint(QPainter::Antialiasing, true); // check if time labels need to be rebuilt - if ((float)m_processor->m_inBlockSize / m_processor->getSampleRate() != m_oldTimePerLine) + if (secondsPerLine() != m_oldSecondsPerLine || m_processor->waterfallHeight() != m_oldHeight) { m_timeTics = makeTimeTics(); - m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate(); + m_oldSecondsPerLine = secondsPerLine(); + m_oldHeight = m_processor->waterfallHeight(); } // print time labels @@ -86,78 +104,104 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); for (auto & line: m_timeTics) { - pos = timeToYPixel(line.first, displayBottom); + pos = timeToYPixel(line.first, m_displayHeight); // align first and last label to the edge if needed, otherwise center them if (line == m_timeTics.front() && pos < label_height / 2) { - painter.drawText(displayLeft - label_width - margin, displayTop - 1, + painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 1, label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip, QString(line.second.c_str())); - painter.drawText(displayRight + margin, displayTop - 1, + painter.drawText(m_displayRight + margin, m_displayTop - 1, label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, QString(line.second.c_str())); } - else if (line == m_timeTics.back() && pos > displayBottom - label_height + 2) + else if (line == m_timeTics.back() && pos > m_displayBottom - label_height + 2) { - painter.drawText(displayLeft - label_width - margin, displayBottom - label_height, + painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height, label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip, QString(line.second.c_str())); - painter.drawText(displayRight + margin, displayBottom - label_height + 2, + painter.drawText(m_displayRight + margin, m_displayBottom - label_height + 2, label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip, QString(line.second.c_str())); } else { - painter.drawText(displayLeft - label_width - margin, pos - label_height / 2, + painter.drawText(m_displayLeft - label_width - margin, pos - label_height / 2, label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, QString(line.second.c_str())); - painter.drawText(displayRight + margin, pos - label_height / 2, + painter.drawText(m_displayRight + margin, pos - label_height / 2, label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, QString(line.second.c_str())); } } // draw the spectrogram precomputed in SaProcessor - if (m_processor->m_waterfallNotEmpty) + if (m_processor->waterfallNotEmpty()) { - QMutexLocker lock(&m_processor->m_dataAccess); - painter.drawImage(displayLeft, displayTop, // top left corner coordinates - QImage(m_processor->m_history.data(), // raw pixel data to display - m_processor->binCount(), // width = number of frequency bins - m_processor->m_waterfallHeight, // height = number of history lines - QImage::Format_RGB32 - ).scaled(displayWidth, // scale to fit view.. - displayBottom, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation)); + QMutexLocker lock(&m_processor->m_reallocationAccess); + QImage temp = QImage(m_processor->getHistory(), // raw pixel data to display + m_processor->waterfallWidth(), // width = number of frequency bins + m_processor->waterfallHeight(), // height = number of history lines + QImage::Format_RGB32); lock.unlock(); + temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution + painter.drawImage(m_displayLeft, m_displayTop, + temp.scaled(m_displayWidth * devicePixelRatio(), + m_displayHeight * devicePixelRatio(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + m_processor->flipRequest(); } else { - painter.fillRect(displayLeft, displayTop, displayWidth, displayBottom, QColor(0,0,0)); + painter.fillRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, QColor(0,0,0)); } + // draw cursor (if it is within bounds) + drawCursor(painter); + // always draw the outline painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawRoundedRect(displayLeft, displayTop, displayWidth, displayBottom, 2.0, 2.0); + painter.drawRoundedRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, 2.0, 2.0); #ifdef SA_DEBUG - // display what FPS would be achieved if waterfall ran in a loop - start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time; + draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time; + m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(displayRight -100, 10, 100, 16, Qt::AlignLeft, - QString(std::string("Max FPS: " + std::to_string(1000000000.0 / start_time)).c_str())); + painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft, + QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms")); #endif } +// Helper functions for time conversion +float SaWaterfallView::samplesPerLine() +{ + return (float)m_processor->inBlockSize() / m_controls->m_windowOverlapModel.value(); +} + +float SaWaterfallView::secondsPerLine() +{ + return samplesPerLine() / m_processor->getSampleRate(); +} + + // Convert time value to Y coordinate for display of given height. float SaWaterfallView::timeToYPixel(float time, int height) { - float pixels_per_line = (float)height / m_processor->m_waterfallHeight; - float seconds_per_line = ((float)m_processor->m_inBlockSize / m_processor->getSampleRate()); + float pixels_per_line = (float)height / m_processor->waterfallHeight(); + + return pixels_per_line * time / secondsPerLine(); +} + + +// Convert Y coordinate on display of given height back to time value. +float SaWaterfallView::yPixelToTime(float position, int height) +{ + if (height == 0) {height = 1;} + float pixels_per_line = (float)height / m_processor->waterfallHeight(); - return pixels_per_line * time / seconds_per_line; + return (position / pixels_per_line) * secondsPerLine(); } @@ -167,16 +211,21 @@ std::vector> SaWaterfallView::makeTimeTics() std::vector> result; float i; - // upper limit defined by number of lines * time per line - float limit = m_processor->m_waterfallHeight * ((float)m_processor->m_inBlockSize / m_processor->getSampleRate()); + // get time value of the last line + float limit = yPixelToTime(m_displayBottom, m_displayHeight); - // set increment so that about 8 tics are generated - float increment = std::round(10 * limit / 7) / 10; + // set increment to about 30 pixels (but min. 0.1 s) + float increment = std::round(10 * limit / (m_displayHeight / 30)) / 10; + if (increment < 0.1) {increment = 0.1;} // NOTE: labels positions are rounded to match the (rounded) label value for (i = 0; i <= limit; i += increment) { - if (i < 10) + if (i > 99) + { + result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 3)); + } + else if (i < 10) { result.emplace_back(std::round(i * 10) / 10, std::to_string(std::round(i * 10) / 10).substr(0, 3)); } @@ -208,10 +257,7 @@ void SaWaterfallView::updateVisibility() if (m_controls->m_waterfallModel.value()) { // clear old data before showing the waterfall - QMutexLocker lock(&m_processor->m_dataAccess); - std::fill(m_processor->m_history.begin(), m_processor->m_history.end(), 0); - lock.unlock(); - + m_processor->clearHistory(); setVisible(true); // increase window size if it is too small @@ -228,3 +274,70 @@ void SaWaterfallView::updateVisibility() } } + +// Draw cursor and its coordinates if it is within display bounds. +void SaWaterfallView::drawCursor(QPainter &painter) +{ + if ( m_cursor.x() >= m_displayLeft + && m_cursor.x() <= m_displayRight + && m_cursor.y() >= m_displayTop + && m_cursor.y() <= m_displayBottom) + { + // cursor lines + painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom)); + painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y())); + + // coordinates: background box + QFontMetrics fontMetrics = painter.fontMetrics(); + unsigned int const box_left = 5; + unsigned int const box_top = 5; + unsigned int const box_margin = 3; + unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 Hz").height() + box_margin); + unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "20000 Hz ").width() + 2*box_margin; + painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top, + box_width, box_height, QColor(0, 0, 0, 64)); + + // coordinates: text + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + QString tmps; + + // frequency + int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); + tmps = QString("%1 Hz").arg(freq); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_margin, + box_width, box_height / 2, Qt::AlignLeft, tmps); + + // time + float time = yPixelToTime(m_cursor.y(), m_displayBottom); + tmps = QString(std::to_string(time).substr(0, 5).c_str()).append(" s"); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_height / 2, + box_width, box_height / 2, Qt::AlignLeft, tmps); + } +} + + +// Handle mouse input: set new cursor position. +// For some reason (a bug?), localPos() only returns integers. As a workaround +// the fractional part is taken from windowPos() (which works correctly). +void SaWaterfallView::mouseMoveEvent(QMouseEvent *event) +{ + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); +} + +void SaWaterfallView::mousePressEvent(QMouseEvent *event) +{ + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); +} + + +// Handle resize event: rebuild time labels +void SaWaterfallView::resizeEvent(QResizeEvent *event) +{ + m_timeTics = makeTimeTics(); +} diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.h b/plugins/SpectrumAnalyzer/SaWaterfallView.h index 0e104c0a168..bd91d6d1641 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.h +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.h @@ -32,6 +32,7 @@ #include "SaControls.h" #include "SaProcessor.h" +class QMouseEvent; // Widget that displays a spectrum waterfall (spectrogram) and time labels. class SaWaterfallView : public QWidget @@ -48,6 +49,9 @@ class SaWaterfallView : public QWidget protected: void paintEvent(QPaintEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; private slots: void periodicUpdate(); @@ -58,9 +62,29 @@ private slots: const EffectControlDialog *m_controlDialog; // Methods and data used to make time labels - float m_oldTimePerLine; + float m_oldSecondsPerLine; + float m_oldHeight; + float samplesPerLine(); + float secondsPerLine(); float timeToYPixel(float time, int height); + float yPixelToTime(float position, int height); std::vector> makeTimeTics(); std::vector> m_timeTics; // 0..n (s) + + // current cursor location and a method to draw it + QPointF m_cursor; + void drawCursor(QPainter &painter); + + // current boundaries for drawing + unsigned int m_displayTop; + unsigned int m_displayBottom; + unsigned int m_displayLeft; + unsigned int m_displayRight; + unsigned int m_displayWidth; + unsigned int m_displayHeight; + + #ifdef SA_DEBUG + float m_execution_avg; + #endif }; #endif // SAWATERFALLVIEW_H diff --git a/plugins/SpectrumAnalyzer/advanced_off.svg b/plugins/SpectrumAnalyzer/advanced_off.svg new file mode 100644 index 00000000000..6d3ed82b105 --- /dev/null +++ b/plugins/SpectrumAnalyzer/advanced_off.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/advanced_on.svg b/plugins/SpectrumAnalyzer/advanced_on.svg new file mode 100644 index 00000000000..9e6b1ca3fb2 --- /dev/null +++ b/plugins/SpectrumAnalyzer/advanced_on.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/advanced_src.svg b/plugins/SpectrumAnalyzer/advanced_src.svg new file mode 100644 index 00000000000..ae201aad0a8 --- /dev/null +++ b/plugins/SpectrumAnalyzer/advanced_src.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ADV. + + + diff --git a/plugins/Vectorscope/CMakeLists.txt b/plugins/Vectorscope/CMakeLists.txt new file mode 100644 index 00000000000..b73ff76d589 --- /dev/null +++ b/plugins/Vectorscope/CMakeLists.txt @@ -0,0 +1,3 @@ +INCLUDE(BuildPlugin) +BUILD_PLUGIN(vectorscope Vectorscope.cpp VecControls.cpp VecControlsDialog.cpp VectorView.cpp +MOCFILES VecControls.h VecControlsDialog.h VectorView.h EMBEDDED_RESOURCES logo.png) diff --git a/plugins/Vectorscope/README.md b/plugins/Vectorscope/README.md new file mode 100644 index 00000000000..18b218f6d9d --- /dev/null +++ b/plugins/Vectorscope/README.md @@ -0,0 +1,14 @@ +# Vectorscope plugin + +## Overview + +Vectorscope is a simple stereo field visualizer. Samples are plotted into a graph, with left and right channels providing the coordinates. Previously drawn samples quickly fade away and are continuously replaced by new samples, creating a real-time plot of the most recently played samples. + +Similar to other effect plugins, the top-level widget is VecControlDialog. It displays configuration knobs and the main VectorView widget. The back-end configuration class is VecControls, which holds all models and configuration values. + +VectorView computes and shows the plot. It gets data for processing from the Vectorscope class, which handles the interface with LMMS. In order to avoid any stalling of the realtime-sensitive audio thread, data are exchanged through a lockless ring buffer. + +## Changelog + + 1.0.0 2019-11-21 + - initial release diff --git a/plugins/Vectorscope/VecControls.cpp b/plugins/Vectorscope/VecControls.cpp new file mode 100644 index 00000000000..0e7a2d06160 --- /dev/null +++ b/plugins/Vectorscope/VecControls.cpp @@ -0,0 +1,70 @@ +/* + * VecControls.cpp - definition of VecControls class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "VecControls.h" + +#include + +#include "VecControlsDialog.h" +#include "Vectorscope.h" + + +VecControls::VecControls(Vectorscope *effect) : + EffectControls(effect), + m_effect(effect), + + // initialize models and set default values + m_persistenceModel(0.5f, 0.0f, 1.0f, 0.05f, this, tr("Display persistence amount")), + m_logarithmicModel(false, this, tr("Logarithmic scale")), + m_highQualityModel(false, this, tr("High quality")) +{ + // Colors (percentages include sRGB gamma correction) + m_colorFG = QColor(60, 255, 130, 255); // ~LMMS green + m_colorGrid = QColor(76, 80, 84, 128); // ~60 % gray (slightly cold / blue), 50 % transparent + m_colorLabels = QColor(76, 80, 84, 255); // ~60 % gray (slightly cold / blue) + m_colorOutline = QColor(30, 34, 38, 255); // ~40 % gray (slightly cold / blue) +} + + +// Create the VecControlDialog widget which handles display of GUI elements. +EffectControlDialog* VecControls::createView() +{ + return new VecControlsDialog(this); +} + + +void VecControls::loadSettings(const QDomElement &element) +{ + m_persistenceModel.loadSettings(element, "Persistence"); + m_logarithmicModel.loadSettings(element, "Logarithmic"); + m_highQualityModel.loadSettings(element, "HighQuality"); +} + + +void VecControls::saveSettings(QDomDocument &document, QDomElement &element) +{ + m_persistenceModel.saveSettings(document, element, "Persistence"); + m_logarithmicModel.saveSettings(document, element, "Logarithmic"); + m_highQualityModel.saveSettings(document, element, "HighQuality"); +} diff --git a/plugins/Vectorscope/VecControls.h b/plugins/Vectorscope/VecControls.h new file mode 100644 index 00000000000..04b688e5a5b --- /dev/null +++ b/plugins/Vectorscope/VecControls.h @@ -0,0 +1,66 @@ +/* + * VecControls.h - declaration of VecControls class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef VECCONTROLS_H +#define VECCONTROLS_H + +#include + +#include "EffectControls.h" + + +class Vectorscope; + +// Holds all the configuration values +class VecControls : public EffectControls +{ + Q_OBJECT +public: + explicit VecControls(Vectorscope *effect); + virtual ~VecControls() {} + + EffectControlDialog *createView() override; + + void saveSettings (QDomDocument &document, QDomElement &element) override; + void loadSettings (const QDomElement &element) override; + + QString nodeName() const override {return "Vectorscope";} + int controlCount() override {return 3;} + +private: + Vectorscope *m_effect; + + FloatModel m_persistenceModel; + BoolModel m_logarithmicModel; + BoolModel m_highQualityModel; + + QColor m_colorFG; + QColor m_colorGrid; + QColor m_colorLabels; + QColor m_colorOutline; + + friend class VecControlsDialog; + friend class VectorView; +}; +#endif // VECCONTROLS_H diff --git a/plugins/Vectorscope/VecControlsDialog.cpp b/plugins/Vectorscope/VecControlsDialog.cpp new file mode 100644 index 00000000000..9916d775605 --- /dev/null +++ b/plugins/Vectorscope/VecControlsDialog.cpp @@ -0,0 +1,94 @@ +/* + * VecControlsDialog.cpp - definition of VecControlsDialog class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "VecControlsDialog.h" + +#include +#include +#include +#include +#include + +#include "embed.h" +#include "LedCheckbox.h" +#include "VecControls.h" +#include "Vectorscope.h" +#include "VectorView.h" + + +// The entire GUI layout is built here. +VecControlsDialog::VecControlsDialog(VecControls *controls) : + EffectControlDialog(controls), + m_controls(controls) +{ + QVBoxLayout *master_layout = new QVBoxLayout; + master_layout->setContentsMargins(0, 2, 0, 0); + setLayout(master_layout); + + // Visualizer widget + // The size of 768 pixels seems to offer a good balance of speed, accuracy and trace thickness. + VectorView *display = new VectorView(controls, m_controls->m_effect->getBuffer(), 768, this); + master_layout->addWidget(display); + + // Config area located inside visualizer + QVBoxLayout *internal_layout = new QVBoxLayout(display); + QHBoxLayout *config_layout = new QHBoxLayout(); + QVBoxLayout *switch_layout = new QVBoxLayout(); + internal_layout->addStretch(); + internal_layout->addLayout(config_layout); + config_layout->addLayout(switch_layout); + + // High-quality switch + LedCheckBox *highQualityButton = new LedCheckBox(tr("HQ"), this); + highQualityButton->setToolTip(tr("Double the resolution and simulate continuous analog-like trace.")); + highQualityButton->setCheckable(true); + highQualityButton->setMinimumSize(70, 12); + highQualityButton->setModel(&controls->m_highQualityModel); + switch_layout->addWidget(highQualityButton); + + // Log. scale switch + LedCheckBox *logarithmicButton = new LedCheckBox(tr("Log. scale"), this); + logarithmicButton->setToolTip(tr("Display amplitude on logarithmic scale to better see small values.")); + logarithmicButton->setCheckable(true); + logarithmicButton->setMinimumSize(70, 12); + logarithmicButton->setModel(&controls->m_logarithmicModel); + switch_layout->addWidget(logarithmicButton); + + config_layout->addStretch(); + + // Persistence knob + Knob *persistenceKnob = new Knob(knobSmall_17, this); + persistenceKnob->setModel(&controls->m_persistenceModel); + persistenceKnob->setLabel(tr("Persist.")); + persistenceKnob->setToolTip(tr("Trace persistence: higher amount means the trace will stay bright for longer time.")); + persistenceKnob->setHintText(tr("Trace persistence"), ""); + config_layout->addWidget(persistenceKnob); +} + + +// Suggest the best widget size. +QSize VecControlsDialog::sizeHint() const +{ + return QSize(275, 300); +} diff --git a/include/update_event.h b/plugins/Vectorscope/VecControlsDialog.h similarity index 57% rename from include/update_event.h rename to plugins/Vectorscope/VecControlsDialog.h index c3773263032..b76c06ad001 100644 --- a/include/update_event.h +++ b/plugins/Vectorscope/VecControlsDialog.h @@ -1,7 +1,7 @@ /* - * update_event.h - signal GUI updates + * VecControlsDialog.h - declatation of VecControlsDialog class. * - * Copyright (c) 2007 Javier Serrano Polo + * Copyright (c) 2019 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -22,23 +22,26 @@ * */ +#ifndef VECCONTROLSDIALOG_H +#define VECCONTROLSDIALOG_H -#ifndef UPDATE_EVENT_H -#define UPDATE_EVENT_H +#include "EffectControlDialog.h" -#include "custom_events.h" +class VecControls; - - -class updateEvent : public QEvent +//! Top-level widget holding the configuration GUI and vector display +class VecControlsDialog : public EffectControlDialog { + Q_OBJECT public: - updateEvent() : - QEvent( (QEvent::Type)customEvents::GUI_UPDATE ) - { - } + explicit VecControlsDialog(VecControls *controls); + virtual ~VecControlsDialog() {} -} ; + bool isResizable() const override {return true;} + QSize sizeHint() const override; +private: + VecControls *m_controls; +}; -#endif +#endif // VECCONTROLSDIALOG_H diff --git a/plugins/Vectorscope/VectorView.cpp b/plugins/Vectorscope/VectorView.cpp new file mode 100644 index 00000000000..9a3f855eb98 --- /dev/null +++ b/plugins/Vectorscope/VectorView.cpp @@ -0,0 +1,328 @@ +/* VectorView.cpp - implementation of VectorView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "VectorView.h" + +#include +#include +#include +#include +#include + +#include "ColorChooser.h" +#include "GuiApplication.h" +#include "MainWindow.h" + + +VectorView::VectorView(VecControls *controls, LocklessRingBuffer *inputBuffer, unsigned short displaySize, QWidget *parent) : + QWidget(parent), + m_controls(controls), + m_inputBuffer(inputBuffer), + m_bufferReader(*inputBuffer), + m_displaySize(displaySize), + m_zoom(1.f), + m_persistTimestamp(0), + m_zoomTimestamp(0), + m_oldHQ(m_controls->m_highQualityModel.value()), + m_oldX(m_displaySize / 2), + m_oldY(m_displaySize / 2) +{ + setMinimumSize(200, 200); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); + + m_displayBuffer.resize(sizeof qRgb(0,0,0) * m_displaySize * m_displaySize, 0); + +#ifdef VEC_DEBUG + m_executionAvg = 0; +#endif +} + + +// Compose and draw all the content; called by Qt. +void VectorView::paintEvent(QPaintEvent *event) +{ +#ifdef VEC_DEBUG + unsigned int drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count(); +#endif + + // All drawing done in this method, local variables are sufficient for the boundary + const int displayTop = 2; + const int displayBottom = height() - 2; + const int displayLeft = 2; + const int displayRight = width() - 2; + const int displayWidth = displayRight - displayLeft; + const int displayHeight = displayBottom - displayTop; + + const float centerX = displayLeft + (displayWidth / 2.f); + const float centerY = displayTop + (displayWidth / 2.f); + + const int margin = 4; + const int gridCorner = 30; + + // Setup QPainter and font sizes + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + QFont normalFont, boldFont; + boldFont.setPixelSize(26); + boldFont.setBold(true); + const int labelWidth = 26; + const int labelHeight = 26; + + bool hq = m_controls->m_highQualityModel.value(); + + // Clear display buffer if quality setting was changed + if (hq != m_oldHQ) + { + m_oldHQ = hq; + for (std::size_t i = 0; i < m_displayBuffer.size(); i++) + { + m_displayBuffer.data()[i] = 0; + } + } + + // Dim stored image based on persistence setting and elapsed time. + // Update period is limited to 50 ms (20 FPS) for non-HQ mode and 10 ms (100 FPS) for HQ mode. + const unsigned int currentTimestamp = std::chrono::duration_cast + ( + std::chrono::high_resolution_clock::now().time_since_epoch() + ).count(); + const unsigned int elapsed = currentTimestamp - m_persistTimestamp; + const unsigned int threshold = hq ? 10 : 50; + if (elapsed > threshold) + { + m_persistTimestamp = currentTimestamp; + // Non-HQ mode uses half the resolution → use limited buffer space. + const std::size_t useableBuffer = hq ? m_displayBuffer.size() : m_displayBuffer.size() / 4; + // The knob value is interpreted on log. scale, otherwise the effect would ramp up too slowly. + // Persistence value specifies fraction of light intensity that remains after 10 ms. + // → Compensate it based on elapsed time (exponential decay). + const float persist = log10(1 + 9 * m_controls->m_persistenceModel.value()); + const float persistPerFrame = pow(persist, elapsed / 10.f); + // Note that for simplicity and performance reasons, this implementation only dims all stored + // values by a given factor. A true simulation would also do the inverse of desaturation that + // occurs in high-intensity traces in HQ mode. + for (std::size_t i = 0; i < useableBuffer; i++) + { + m_displayBuffer.data()[i] *= persistPerFrame; + } + } + + // Get new samples from the lockless input FIFO buffer + auto inBuffer = m_bufferReader.read_max(m_inputBuffer->capacity()); + std::size_t frameCount = inBuffer.size(); + + // Draw new points on top + float left, right; + int x, y; + + const bool logScale = m_controls->m_logarithmicModel.value(); + const unsigned short activeSize = hq ? m_displaySize : m_displaySize / 2; + + // Helper lambda functions for better readability + // Make sure pixel stays within display bounds: + auto saturate = [=](short pixelPos) {return qBound((short)0, pixelPos, (short)(activeSize - 1));}; + // Take existing pixel and brigthen it. Very bright light should reduce saturation and become + // white. This effect is easily approximated by capping elementary colors to 255 individually. + auto updatePixel = [&](unsigned short x, unsigned short y, QColor addedColor) + { + QColor currentColor = ((QRgb*)m_displayBuffer.data())[x + y * activeSize]; + currentColor.setRed(std::min(currentColor.red() + addedColor.red(), 255)); + currentColor.setGreen(std::min(currentColor.green() + addedColor.green(), 255)); + currentColor.setBlue(std::min(currentColor.blue() + addedColor.blue(), 255)); + ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = currentColor.rgb(); + }; + + if (hq) + { + // High quality mode: check distance between points and draw a line. + // The longer the line is, the dimmer, simulating real electron trace on luminescent screen. + for (std::size_t frame = 0; frame < frameCount; frame++) + { + float inLeft = inBuffer[frame][0] * m_zoom; + float inRight = inBuffer[frame][1] * m_zoom; + // Scale left and right channel from (-1.0, 1.0) to display range + if (logScale) + { + // To better preserve shapes, the log scale is applied to the distance from origin, + // not the individual channels. + const float distance = sqrt(inLeft * inLeft + inRight * inRight); + const float distanceLog = log10(1 + 9 * abs(distance)); + const float angleCos = inLeft / distance; + const float angleSin = inRight / distance; + left = distanceLog * angleCos * (activeSize - 1) / 4; + right = distanceLog * angleSin * (activeSize - 1) / 4; + } + else + { + left = inLeft * (activeSize - 1) / 4; + right = inRight * (activeSize - 1) / 4; + } + + // Rotate display coordinates 45 degrees, flip Y axis and make sure the result stays within bounds + x = saturate(right - left + activeSize / 2.f); + y = saturate(activeSize - (right + left + activeSize / 2.f)); + + // Estimate number of points needed to fill space between the old and new pixel. Cap at 100. + unsigned char points = std::min((int)sqrt((m_oldX - x) * (m_oldX - x) + (m_oldY - y) * (m_oldY - y)), 100); + + // Large distance = dim trace. The curve for darker() is choosen so that: + // - no movement (0 points) actually _increases_ brightness slightly, + // - one point between samples = returns exactly the specified color, + // - one to 99 points between samples = follows a sharp "1/x" decaying curve, + // - 100 points between samples = returns approximately 5 % brightness. + // Everything else is discarded (by the 100 point cap) because there is not much to see anyway. + QColor addedColor = m_controls->m_colorFG.darker(75 + 20 * points).rgb(); + + // Draw the new pixel: the beam sweeps across area that may have been excited before + // → add new value to existing pixel state. + updatePixel(x, y, addedColor); + + // Draw interpolated points between the old pixel and the new one + int newX = right - left + activeSize / 2.f; + int newY = activeSize - (right + left + activeSize / 2.f); + for (unsigned char i = 1; i < points; i++) + { + x = saturate(((points - i) * m_oldX + i * newX) / points); + y = saturate(((points - i) * m_oldY + i * newY) / points); + updatePixel(x, y, addedColor); + } + m_oldX = newX; + m_oldY = newY; + } + } + else + { + // To improve performance, non-HQ mode uses smaller display size and only + // one full-color pixel per sample. + for (std::size_t frame = 0; frame < frameCount; frame++) + { + float inLeft = inBuffer[frame][0] * m_zoom; + float inRight = inBuffer[frame][1] * m_zoom; + if (logScale) { + const float distance = sqrt(inLeft * inLeft + inRight * inRight); + const float distanceLog = log10(1 + 9 * abs(distance)); + const float angleCos = inLeft / distance; + const float angleSin = inRight / distance; + left = distanceLog * angleCos * (activeSize - 1) / 4; + right = distanceLog * angleSin * (activeSize - 1) / 4; + } else { + left = inLeft * (activeSize - 1) / 4; + right = inRight * (activeSize - 1) / 4; + } + x = saturate(right - left + activeSize / 2.f); + y = saturate(activeSize - (right + left + activeSize / 2.f)); + ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = m_controls->m_colorFG.rgb(); + } + } + + // Draw background + painter.fillRect(displayLeft, displayTop, displayWidth, displayHeight, QColor(0,0,0)); + + // Draw the final image + QImage temp = QImage(m_displayBuffer.data(), + activeSize, + activeSize, + QImage::Format_RGB32); + temp.setDevicePixelRatio(devicePixelRatio()); + painter.drawImage(displayLeft, displayTop, + temp.scaledToWidth(displayWidth * devicePixelRatio(), + Qt::SmoothTransformation)); + + // Draw the grid and labels + painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawEllipse(QPointF(centerX, centerY), displayWidth / 2.f, displayWidth / 2.f); + painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::DotLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawLine(QPointF(centerX, centerY), QPointF(displayLeft + gridCorner, displayTop + gridCorner)); + painter.drawLine(QPointF(centerX, centerY), QPointF(displayRight - gridCorner, displayTop + gridCorner)); + + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.setFont(boldFont); + painter.drawText(displayLeft + margin, displayTop, + labelWidth, labelHeight, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, + QString("L")); + painter.drawText(displayRight - margin - labelWidth, displayTop, + labelWidth, labelHeight, Qt::AlignRight| Qt::AlignTop | Qt::TextDontClip, + QString("R")); + + // Draw the outline + painter.setPen(QPen(m_controls->m_colorOutline, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawRoundedRect(1, 1, width() - 2, height() - 2, 2.f, 2.f); + + // Draw zoom info if changed within last second (re-using timestamp acquired for dimming) + if (currentTimestamp - m_zoomTimestamp < 1000) + { + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.setFont(normalFont); + painter.drawText(displayWidth / 2 - 50, displayBottom - 20, 100, 16, Qt::AlignCenter, + QString("Zoom: ").append(std::to_string((int)round(m_zoom * 100)).c_str()).append(" %")); + } + + // Optionally measure drawing performance +#ifdef VEC_DEBUG + drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count() - drawTime; + m_executionAvg = 0.95f * m_executionAvg + 0.05f * drawTime / 1000000.f; + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.setFont(normalFont); + painter.drawText(displayWidth / 2 - 50, displayBottom - 16, 100, 16, Qt::AlignLeft, + QString("Exec avg.: ").append(std::to_string(m_executionAvg).substr(0, 5).c_str()).append(" ms")); +#endif +} + + +// Periodically trigger repaint and check if the widget is visible +void VectorView::periodicUpdate() +{ + m_visible = isVisible(); + if (m_visible) {update();} +} + + +// Allow to change color on double-click. +// More of an Easter egg, to avoid cluttering the interface with non-essential functionality. +void VectorView::mouseDoubleClickEvent(QMouseEvent *event) +{ + ColorChooser *colorDialog = new ColorChooser(m_controls->m_colorFG, this); + if (colorDialog->exec()) + { + m_controls->m_colorFG = colorDialog->currentColor(); + } +} + + +// Change zoom level using the mouse wheel +void VectorView::wheelEvent(QWheelEvent *event) +{ + // Go through integers to avoid accumulating errors + const unsigned short old_zoom = round(100 * m_zoom); + // Min-max bounds are 20 and 1000 %, step for 15°-increment mouse wheel is 20 % + const unsigned short new_zoom = qBound(20, old_zoom + event->angleDelta().y() / 6, 1000); + m_zoom = new_zoom / 100.f; + event->accept(); + m_zoomTimestamp = std::chrono::duration_cast + ( + std::chrono::high_resolution_clock::now().time_since_epoch() + ).count(); + +} diff --git a/plugins/Vectorscope/VectorView.h b/plugins/Vectorscope/VectorView.h new file mode 100644 index 00000000000..066e306a0a0 --- /dev/null +++ b/plugins/Vectorscope/VectorView.h @@ -0,0 +1,80 @@ +/* VectorView.h - declaration of VectorView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ +#ifndef VECTORVIEW_H +#define VECTORVIEW_H + +#include +#include +#include + +#include "Knob.h" +#include "LedCheckbox.h" +#include "LocklessRingBuffer.h" +#include "VecControls.h" + +//#define VEC_DEBUG + + +// Widget that displays a vectorscope visualization of stereo signal. +class VectorView : public QWidget +{ + Q_OBJECT +public: + explicit VectorView(VecControls *controls, LocklessRingBuffer *inputBuffer, unsigned short displaySize, QWidget *parent = 0); + virtual ~VectorView() {} + + QSize sizeHint() const override {return QSize(300, 300);} + +protected: + void paintEvent(QPaintEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + +private slots: + void periodicUpdate(); + +private: + VecControls *m_controls; + + LocklessRingBuffer *m_inputBuffer; + LocklessRingBufferReader m_bufferReader; + + std::vector m_displayBuffer; + const unsigned short m_displaySize; + + bool m_visible; + + float m_zoom; + + // State variables for comparison with previous repaint + unsigned int m_persistTimestamp; + unsigned int m_zoomTimestamp; + bool m_oldHQ; + int m_oldX; + int m_oldY; + +#ifdef VEC_DEBUG + float m_executionAvg = 0; +#endif +}; +#endif // VECTORVIEW_H diff --git a/plugins/Vectorscope/Vectorscope.cpp b/plugins/Vectorscope/Vectorscope.cpp new file mode 100644 index 00000000000..f8bc30c40df --- /dev/null +++ b/plugins/Vectorscope/Vectorscope.cpp @@ -0,0 +1,80 @@ +/* + * Vectorscope.cpp - definition of Vectorscope class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Vectorscope.h" + +#include "embed.h" +#include "plugin_export.h" + + +extern "C" { + Plugin::Descriptor PLUGIN_EXPORT vectorscope_plugin_descriptor = + { + STRINGIFY(PLUGIN_NAME), + "Vectorscope", + QT_TRANSLATE_NOOP("pluginBrowser", "A stereo field visualizer."), + "Martin Pavelek ", + 0x0100, + Plugin::Effect, + new PluginPixmapLoader("logo"), + NULL, + NULL + }; +} + + +Vectorscope::Vectorscope(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) : + Effect(&vectorscope_plugin_descriptor, parent, key), + m_controls(this), + // Buffer is sized to cover 4* the current maximum LMMS audio buffer size, + // so that it has some reserve space in case GUI thresd is busy. + m_inputBuffer(4 * m_maxBufferSize) +{ +} + + +// Take audio data and store them for processing and display in the GUI thread. +bool Vectorscope::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) +{ + if (!isEnabled() || !isRunning ()) {return false;} + + // Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. + if (m_controls.isViewVisible()) + { + // To avoid processing spikes on audio thread, data are stored in + // a lockless ringbuffer and processed in a separate thread. + m_inputBuffer.write(buffer, frame_count); + } + return isRunning(); +} + + +extern "C" { + // needed for getting plugin out of shared lib + PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *parent, void *data) + { + return new Vectorscope(parent, static_cast(data)); + } +} + diff --git a/plugins/Vectorscope/Vectorscope.h b/plugins/Vectorscope/Vectorscope.h new file mode 100644 index 00000000000..b45ff6de4ab --- /dev/null +++ b/plugins/Vectorscope/Vectorscope.h @@ -0,0 +1,52 @@ +/* Vectorscope.h - declaration of Vectorscope class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef VECTORSCOPE_H +#define VECTORSCOPE_H + +#include "Effect.h" +#include "LocklessRingBuffer.h" +#include "VecControls.h" + + +//! Top level class; handles LMMS interface and accumulates data for processing. +class Vectorscope : public Effect +{ +public: + Vectorscope(Model *parent, const Descriptor::SubPluginFeatures::Key *key); + virtual ~Vectorscope() {}; + + bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override; + EffectControls *controls() override {return &m_controls;} + LocklessRingBuffer *getBuffer() {return &m_inputBuffer;} + +private: + VecControls m_controls; + + // Maximum LMMS buffer size (hard coded, the actual constant is hard to get) + const unsigned int m_maxBufferSize = 4096; + LocklessRingBuffer m_inputBuffer; +}; + +#endif // VECTORSCOPE_H + diff --git a/plugins/Vectorscope/logo.png b/plugins/Vectorscope/logo.png new file mode 100644 index 00000000000..9340da708dd Binary files /dev/null and b/plugins/Vectorscope/logo.png differ diff --git a/plugins/Xpressive/Xpressive.cpp b/plugins/Xpressive/Xpressive.cpp index 018319c823f..a80a0ae4152 100644 --- a/plugins/Xpressive/Xpressive.cpp +++ b/plugins/Xpressive/Xpressive.cpp @@ -53,9 +53,9 @@ extern "C" { Plugin::Descriptor PLUGIN_EXPORT xpressive_plugin_descriptor = { STRINGIFY( - PLUGIN_NAME), "X-Pressive", QT_TRANSLATE_NOOP("pluginBrowser", - "Mathematical expression parser"), "Orr Dvori", 0x0100, - Plugin::Instrument, new PluginPixmapLoader("logo"), NULL, NULL }; + PLUGIN_NAME), "Xpressive", QT_TRANSLATE_NOOP("pluginBrowser", + "Mathematical expression parser"), "Orr Dvori", 0x0100, + Plugin::Instrument, new PluginPixmapLoader("logo"), NULL, NULL }; } @@ -257,7 +257,6 @@ class XpressiveKnob: public Knob { setCenterPointY(14.5); setInnerRadius(4); setOuterRadius(9); - setOuterColor(QColor(0x519fff)); setTotalAngle(300.0); setLineWidth(3); } @@ -277,14 +276,18 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : InstrumentViewFixedSize(_instrument, _parent) { - const int COL_KNOBS = 194; - const int ROW_KNOBSA1 = 26; - const int ROW_KNOBSA2 = 26 + 32; - const int ROW_KNOBSA3 = 26 + 64; - const int ROW_KNOBSP1 = 126; - const int ROW_KNOBSP2 = 126 + 32; - const int ROW_KNOBREL = 126 + 64; - const int ROW_WAVEBTN = 234; + const int COL_KNOBS = 191; + const int BASE_START = 2; + const int ROW_KNOBSA1 = BASE_START; + const int ROW_KNOBSA2 = BASE_START + 32; + const int ROW_KNOBSA3 = BASE_START + 64; + const int ROW_KNOBSP1 = BASE_START + 100; + const int ROW_KNOBSP2 = BASE_START + 100 + 32; + const int ROW_KNOBREL = BASE_START + 100 + 64; + const int ROW_BTN = BASE_START + 85; + const int ROW_WAVEBTN = BASE_START + 233 - 26; + const int EXPR_TEXT_Y = BASE_START + 102; + const int EXPR_TEXT_H = 90; setAutoFillBackground(true); QPalette pal; @@ -293,7 +296,7 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : setPalette(pal); m_graph = new Graph(this, Graph::LinearStyle, 180, 81); - m_graph->move(9, 27); + m_graph->move(3, BASE_START + 1); m_graph->setAutoFillBackground(true); m_graph->setGraphColor(QColor(255, 255, 255)); m_graph->setEnabled(false); @@ -313,37 +316,37 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : PixmapButton * m_helpBtn; m_w1Btn = new PixmapButton(this, NULL); - m_w1Btn->move(9, 111); + m_w1Btn->move(3, ROW_BTN); m_w1Btn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("w1_active")); m_w1Btn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("w1_inactive")); ToolTip::add(m_w1Btn, tr("Select oscillator W1")); m_w2Btn = new PixmapButton(this, NULL); - m_w2Btn->move(32, 111); + m_w2Btn->move(26, ROW_BTN); m_w2Btn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("w2_active")); m_w2Btn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("w2_inactive")); ToolTip::add(m_w2Btn, tr("Select oscillator W2")); m_w3Btn = new PixmapButton(this, NULL); - m_w3Btn->move(55, 111); + m_w3Btn->move(49, ROW_BTN); m_w3Btn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("w3_active")); m_w3Btn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("w3_inactive")); ToolTip::add(m_w3Btn, tr("Select oscillator W3")); m_o1Btn = new PixmapButton(this, NULL); - m_o1Btn->move(85, 111); + m_o1Btn->move(79, ROW_BTN); m_o1Btn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("o1_active")); m_o1Btn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("o1_inactive")); ToolTip::add(m_o1Btn, tr("Select output O1")); m_o2Btn = new PixmapButton(this, NULL); - m_o2Btn->move(107, 111); + m_o2Btn->move(101, ROW_BTN); m_o2Btn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("o2_active")); m_o2Btn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("o2_inactive")); ToolTip::add(m_o2Btn, tr("Select output O2")); m_helpBtn = new PixmapButton(this, NULL); - m_helpBtn->move(139, 111); + m_helpBtn->move(133, ROW_BTN); m_helpBtn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("help_active")); m_helpBtn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("help_inactive")); ToolTip::add(m_helpBtn, tr("Open help window")); @@ -359,38 +362,38 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : m_selectedGraphGroup->setModel(&e->selectedGraph()); m_sinWaveBtn = new PixmapButton(this, tr("Sine wave")); - m_sinWaveBtn->move(10, ROW_WAVEBTN); + m_sinWaveBtn->move(4, ROW_WAVEBTN); m_sinWaveBtn->setActiveGraphic(embed::getIconPixmap("sin_wave_active")); m_sinWaveBtn->setInactiveGraphic(embed::getIconPixmap("sin_wave_inactive")); ToolTip::add(m_sinWaveBtn, tr("Sine wave")); m_moogWaveBtn = new PixmapButton(this, tr("Moog-saw wave")); - m_moogWaveBtn->move(10, ROW_WAVEBTN-14); + m_moogWaveBtn->move(4, ROW_WAVEBTN-14); m_moogWaveBtn->setActiveGraphic( embed::getIconPixmap( "moog_saw_wave_active" ) ); m_moogWaveBtn->setInactiveGraphic(embed::getIconPixmap("moog_saw_wave_inactive")); ToolTip::add(m_moogWaveBtn, tr("Moog-saw wave")); m_expWaveBtn = new PixmapButton(this, tr("Exponential wave")); - m_expWaveBtn->move(10 +14, ROW_WAVEBTN-14); + m_expWaveBtn->move(4 +14, ROW_WAVEBTN-14); m_expWaveBtn->setActiveGraphic(embed::getIconPixmap( "exp_wave_active" ) ); m_expWaveBtn->setInactiveGraphic(embed::getIconPixmap( "exp_wave_inactive" ) ); ToolTip::add(m_expWaveBtn, tr("Exponential wave")); m_sawWaveBtn = new PixmapButton(this, tr("Saw wave")); - m_sawWaveBtn->move(10 + 14 * 2, ROW_WAVEBTN-14); + m_sawWaveBtn->move(4 + 14 * 2, ROW_WAVEBTN-14); m_sawWaveBtn->setActiveGraphic(embed::getIconPixmap("saw_wave_active")); m_sawWaveBtn->setInactiveGraphic(embed::getIconPixmap("saw_wave_inactive")); ToolTip::add(m_sawWaveBtn, tr("Saw wave")); m_usrWaveBtn = new PixmapButton(this, tr("User-defined wave")); - m_usrWaveBtn->move(10 + 14 * 3, ROW_WAVEBTN-14); + m_usrWaveBtn->move(4 + 14 * 3, ROW_WAVEBTN-14); m_usrWaveBtn->setActiveGraphic(embed::getIconPixmap("usr_wave_active")); m_usrWaveBtn->setInactiveGraphic(embed::getIconPixmap("usr_wave_inactive")); ToolTip::add(m_usrWaveBtn, tr("User-defined wave")); m_triangleWaveBtn = new PixmapButton(this, tr("Triangle wave")); - m_triangleWaveBtn->move(10 + 14, ROW_WAVEBTN); + m_triangleWaveBtn->move(4 + 14, ROW_WAVEBTN); m_triangleWaveBtn->setActiveGraphic( embed::getIconPixmap("triangle_wave_active")); m_triangleWaveBtn->setInactiveGraphic( @@ -398,14 +401,14 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : ToolTip::add(m_triangleWaveBtn, tr("Triangle wave")); m_sqrWaveBtn = new PixmapButton(this, tr("Square wave")); - m_sqrWaveBtn->move(10 + 14 * 2, ROW_WAVEBTN); + m_sqrWaveBtn->move(4 + 14 * 2, ROW_WAVEBTN); m_sqrWaveBtn->setActiveGraphic(embed::getIconPixmap("square_wave_active")); m_sqrWaveBtn->setInactiveGraphic( embed::getIconPixmap("square_wave_inactive")); ToolTip::add(m_sqrWaveBtn, tr("Square wave")); m_whiteNoiseWaveBtn = new PixmapButton(this, tr("White noise")); - m_whiteNoiseWaveBtn->move(10 + 14 * 3, ROW_WAVEBTN); + m_whiteNoiseWaveBtn->move(4 + 14 * 3, ROW_WAVEBTN); m_whiteNoiseWaveBtn->setActiveGraphic( embed::getIconPixmap("white_noise_wave_active")); m_whiteNoiseWaveBtn->setInactiveGraphic( @@ -415,16 +418,16 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : m_waveInterpolate = new LedCheckBox("Interpolate", this, tr("WaveInterpolate"), LedCheckBox::Green); - m_waveInterpolate->move(120, 230); + m_waveInterpolate->move(2, 230); m_expressionValidToggle = new LedCheckBox("", this, tr("ExpressionValid"), LedCheckBox::Red); - m_expressionValidToggle->move(174, 216); + m_expressionValidToggle->move(168, EXPR_TEXT_Y+EXPR_TEXT_H-2); m_expressionValidToggle->setEnabled( false ); m_expressionEditor = new QPlainTextEdit(this); - m_expressionEditor->move(9, 128); - m_expressionEditor->resize(180, 90); + m_expressionEditor->move(3, EXPR_TEXT_Y); + m_expressionEditor->resize(180, EXPR_TEXT_H); m_generalPurposeKnob[0] = new XpressiveKnob(this,"A1"); m_generalPurposeKnob[0]->setHintText(tr("General purpose 1:"), ""); @@ -452,9 +455,16 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : - m_smoothKnob=new Knob(this,"Smoothness"); + m_smoothKnob=new Knob(knobStyled, this, "Smoothness"); + m_smoothKnob->setFixedSize(25, 25); + m_smoothKnob->setCenterPointX(12.5); + m_smoothKnob->setCenterPointY(12.5); + m_smoothKnob->setInnerRadius(4); + m_smoothKnob->setOuterRadius(9); + m_smoothKnob->setTotalAngle(280.0); + m_smoothKnob->setLineWidth(3); m_smoothKnob->setHintText(tr("Smoothness"), ""); - m_smoothKnob->move(80, 220); + m_smoothKnob->move(66, EXPR_TEXT_Y + EXPR_TEXT_H + 4); connect(m_generalPurposeKnob[0], SIGNAL(sliderMoved(float)), this, SLOT(expressionChanged())); @@ -748,7 +758,7 @@ void XpressiveView::updateLayout() { void XpressiveView::sinWaveClicked() { if (m_output_expr) - m_expressionEditor->appendPlainText("sinew(t*f)"); + m_expressionEditor->appendPlainText("sinew(integrate(f))"); else m_expressionEditor->appendPlainText("sinew(t)"); Engine::getSong()->setModified(); @@ -756,7 +766,7 @@ void XpressiveView::sinWaveClicked() { void XpressiveView::triangleWaveClicked() { if (m_output_expr) - m_expressionEditor->appendPlainText("trianglew(t*f)"); + m_expressionEditor->appendPlainText("trianglew(integrate(f))"); else m_expressionEditor->appendPlainText("trianglew(t)"); Engine::getSong()->setModified(); @@ -764,7 +774,7 @@ void XpressiveView::triangleWaveClicked() { void XpressiveView::sawWaveClicked() { if (m_output_expr) - m_expressionEditor->appendPlainText("saww(t*f)"); + m_expressionEditor->appendPlainText("saww(integrate(f))"); else m_expressionEditor->appendPlainText("saww(t)"); Engine::getSong()->setModified(); @@ -772,7 +782,7 @@ void XpressiveView::sawWaveClicked() { void XpressiveView::sqrWaveClicked() { if (m_output_expr) - m_expressionEditor->appendPlainText("squarew(t*f)"); + m_expressionEditor->appendPlainText("squarew(integrate(f))"); else m_expressionEditor->appendPlainText("squarew(t)"); Engine::getSong()->setModified(); @@ -786,7 +796,7 @@ void XpressiveView::noiseWaveClicked() { void XpressiveView::moogSawWaveClicked() { if (m_output_expr) - m_expressionEditor->appendPlainText("moogsaww(t*f)"); + m_expressionEditor->appendPlainText("moogsaww(integrate(f))"); else m_expressionEditor->appendPlainText("moogsaww(t)"); Engine::getSong()->setModified(); @@ -794,7 +804,7 @@ void XpressiveView::moogSawWaveClicked() void XpressiveView::expWaveClicked() { if (m_output_expr) - m_expressionEditor->appendPlainText("expw(t*f)"); + m_expressionEditor->appendPlainText("expw(integrate(f))"); else m_expressionEditor->appendPlainText("expw(t)"); Engine::getSong()->setModified(); @@ -861,7 +871,7 @@ QString XpressiveHelpView::s_helpText= XpressiveHelpView::XpressiveHelpView():QTextEdit(s_helpText) { - setWindowTitle ( "X-Pressive Help" ); + setWindowTitle ( "Xpressive Help" ); setTextInteractionFlags ( Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse ); gui->mainWindow()->addWindowedWidget( this ); parentWidget()->setAttribute( Qt::WA_DeleteOnClose, false ); diff --git a/plugins/Xpressive/Xpressive.svg b/plugins/Xpressive/Xpressive.svg new file mode 100644 index 00000000000..ef3029c0d90 --- /dev/null +++ b/plugins/Xpressive/Xpressive.svg @@ -0,0 +1,130 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/Xpressive/Xpressive_logo.svg b/plugins/Xpressive/Xpressive_logo.svg new file mode 100644 index 00000000000..fca1f0d981b --- /dev/null +++ b/plugins/Xpressive/Xpressive_logo.svg @@ -0,0 +1,106 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/plugins/Xpressive/artwork.png b/plugins/Xpressive/artwork.png index d5b39acee2e..1f4e35ad51e 100644 Binary files a/plugins/Xpressive/artwork.png and b/plugins/Xpressive/artwork.png differ diff --git a/plugins/Xpressive/help_active.png b/plugins/Xpressive/help_active.png index 4e991ad54af..8d8b49d4327 100644 Binary files a/plugins/Xpressive/help_active.png and b/plugins/Xpressive/help_active.png differ diff --git a/plugins/Xpressive/help_inactive.png b/plugins/Xpressive/help_inactive.png index 4b77af24fb3..d08f6d9a7b5 100644 Binary files a/plugins/Xpressive/help_inactive.png and b/plugins/Xpressive/help_inactive.png differ diff --git a/plugins/Xpressive/logo.png b/plugins/Xpressive/logo.png index 68b613176d6..555c3f13e68 100644 Binary files a/plugins/Xpressive/logo.png and b/plugins/Xpressive/logo.png differ diff --git a/plugins/Xpressive/o1_active.png b/plugins/Xpressive/o1_active.png index 6370769dd27..8e31a8322c0 100644 Binary files a/plugins/Xpressive/o1_active.png and b/plugins/Xpressive/o1_active.png differ diff --git a/plugins/Xpressive/o1_inactive.png b/plugins/Xpressive/o1_inactive.png index 0fd4f8b2989..7a6b603e3e7 100644 Binary files a/plugins/Xpressive/o1_inactive.png and b/plugins/Xpressive/o1_inactive.png differ diff --git a/plugins/Xpressive/o2_active.png b/plugins/Xpressive/o2_active.png index 51897412d42..4e3b5f214d7 100644 Binary files a/plugins/Xpressive/o2_active.png and b/plugins/Xpressive/o2_active.png differ diff --git a/plugins/Xpressive/o2_inactive.png b/plugins/Xpressive/o2_inactive.png index 20158d72575..35851d5533f 100644 Binary files a/plugins/Xpressive/o2_inactive.png and b/plugins/Xpressive/o2_inactive.png differ diff --git a/plugins/Xpressive/w1_active.png b/plugins/Xpressive/w1_active.png index 49a3a2b6ec8..68d8e9ae088 100644 Binary files a/plugins/Xpressive/w1_active.png and b/plugins/Xpressive/w1_active.png differ diff --git a/plugins/Xpressive/w1_inactive.png b/plugins/Xpressive/w1_inactive.png index 266e4206fa7..7159b49799d 100644 Binary files a/plugins/Xpressive/w1_inactive.png and b/plugins/Xpressive/w1_inactive.png differ diff --git a/plugins/Xpressive/w2_active.png b/plugins/Xpressive/w2_active.png index cd4729949e1..17d7b53d935 100644 Binary files a/plugins/Xpressive/w2_active.png and b/plugins/Xpressive/w2_active.png differ diff --git a/plugins/Xpressive/w2_inactive.png b/plugins/Xpressive/w2_inactive.png index 8d54929da7b..412482bb0e4 100644 Binary files a/plugins/Xpressive/w2_inactive.png and b/plugins/Xpressive/w2_inactive.png differ diff --git a/plugins/Xpressive/w3_active.png b/plugins/Xpressive/w3_active.png index 9e4facb2cfa..579b997dd69 100644 Binary files a/plugins/Xpressive/w3_active.png and b/plugins/Xpressive/w3_active.png differ diff --git a/plugins/Xpressive/w3_inactive.png b/plugins/Xpressive/w3_inactive.png index e5b0bc7d9e1..d45d8b497ab 100644 Binary files a/plugins/Xpressive/w3_inactive.png and b/plugins/Xpressive/w3_inactive.png differ diff --git a/plugins/Xpressive/wavegraph.png b/plugins/Xpressive/wavegraph.png index 9d58e8fe732..ea7bc3761f1 100644 Binary files a/plugins/Xpressive/wavegraph.png and b/plugins/Xpressive/wavegraph.png differ diff --git a/plugins/lb302/README b/plugins/lb302/README index 2a8058e56d8..b556d80d406 100644 --- a/plugins/lb302/README +++ b/plugins/lb302/README @@ -15,7 +15,7 @@ BUG: to be caused by 'unexhausted buffers' That is, the problem manifests itself to a greater degree when the user "buffer size" configuration is increased to over 1024 frames or so. The problem is much less - noticable when the buffer size is set to 64 frames. + noticeable when the buffer size is set to 64 frames. BUG: The synth does not make accomodations for sampling rates other than diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index 473e7702f09..bdc4a4d8690 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -10,3 +10,22 @@ ENDIF() ADD_SUBDIRECTORY(rpmalloc) ADD_SUBDIRECTORY(weakjack) + +# The lockless ring buffer library is compiled as part of the core +SET(RINGBUFFER_DIR "${CMAKE_SOURCE_DIR}/src/3rdparty/ringbuffer/") +SET(RINGBUFFER_DIR ${RINGBUFFER_DIR} PARENT_SCOPE) +# Create a dummy ringbuffer_export.h, since ringbuffer is not compiled as a library +FILE(WRITE ${CMAKE_BINARY_DIR}/src/ringbuffer_export.h + "#include \"${CMAKE_BINARY_DIR}/src/lmms_export.h\"\n + #define RINGBUFFER_EXPORT LMMS_EXPORT") +# Enable MLOCK support for ringbuffer if available +INCLUDE(CheckIncludeFiles) +CHECK_INCLUDE_FILES(sys/mman.h HAVE_SYS_MMAN) +IF(HAVE_SYS_MMAN) + SET(USE_MLOCK ON) +ELSE() + SET(USE_MLOCK OFF) +ENDIF() +# Generate ringbuffer configuration headers +CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-config.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-config.h) +CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-version.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-version.h) diff --git a/src/3rdparty/ringbuffer b/src/3rdparty/ringbuffer new file mode 160000 index 00000000000..82ed7cfb9ad --- /dev/null +++ b/src/3rdparty/ringbuffer @@ -0,0 +1 @@ +Subproject commit 82ed7cfb9ad40467421d8b14ca1af0350e92613c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed0deefd1a1..59710926d86 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ INCLUDE_DIRECTORIES( "${CMAKE_BINARY_DIR}/include" "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/include" + "${RINGBUFFER_DIR}/include" ) IF(WIN32 AND MSVC) @@ -89,6 +90,8 @@ IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}") ENDIF() +LIST(APPEND LMMS_SRCS "${RINGBUFFER_DIR}/src/lib/ringbuffer.cpp") + # Use libraries in non-standard directories (e.g., another version of Qt) IF(LMMS_BUILD_LINUX) LINK_LIBRARIES(-Wl,--enable-new-dtags) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ba41e089c7a..f1c183e3f55 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,5 +1,6 @@ set(LMMS_SRCS ${LMMS_SRCS} + core/AutomatableModel.cpp core/AutomationPattern.cpp core/BandLimitedWave.cpp @@ -30,6 +31,7 @@ set(LMMS_SRCS core/LadspaControl.cpp core/LadspaManager.cpp core/LfoController.cpp + core/LinkedModelGroups.cpp core/LocklessAllocator.cpp core/MemoryHelper.cpp core/MemoryManager.cpp diff --git a/src/core/FxMixer.cpp b/src/core/FxMixer.cpp index 032090bf1d5..f04435e0523 100644 --- a/src/core/FxMixer.cpp +++ b/src/core/FxMixer.cpp @@ -336,6 +336,11 @@ void FxMixer::deleteChannel( int index ) deleteChannelSend( ch->m_receives.first() ); } + // if m_lastSoloed was our index, reset it + if (m_lastSoloed == index) { m_lastSoloed = -1; } + // if m_lastSoloed is > delete index, it will move left + else if (m_lastSoloed > index) { --m_lastSoloed; } + // actually delete the channel m_fxChannels.remove(index); delete ch; @@ -373,6 +378,10 @@ void FxMixer::moveChannelLeft( int index ) // channels to swap int a = index - 1, b = index; + // check if m_lastSoloed is one of our swaps + if (m_lastSoloed == a) { m_lastSoloed = b; } + else if (m_lastSoloed == b) { m_lastSoloed = a; } + // go through every instrument and adjust for the channel index change QVector songTrackList = Engine::getSong()->tracks(); QVector bbTrackList = Engine::getBBTrackContainer()->tracks(); diff --git a/src/core/LinkedModelGroups.cpp b/src/core/LinkedModelGroups.cpp new file mode 100644 index 00000000000..c9bbc475a8f --- /dev/null +++ b/src/core/LinkedModelGroups.cpp @@ -0,0 +1,185 @@ +/* + * LinkedModelGroups.cpp - base classes for groups of linked models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "LinkedModelGroups.h" + +#include +#include + +#include "AutomatableModel.h" +#include "stdshims.h" + + + + +/* + LinkedModelGroup +*/ + + +void LinkedModelGroup::linkControls(LinkedModelGroup *other) +{ + foreach_model([&other](const std::string& id, ModelInfo& inf) + { + auto itr2 = other->m_models.find(id); + Q_ASSERT(itr2 != other->m_models.end()); + AutomatableModel::linkModels(inf.m_model, itr2->second.m_model); + }); +} + + + + +void LinkedModelGroup::saveValues(QDomDocument &doc, QDomElement &that) +{ + foreach_model([&doc, &that](const std::string& , ModelInfo& inf) + { + inf.m_model->saveSettings(doc, that, /*m_models[idx].m_name*/ inf.m_name); /* TODO: m_name useful */ + }); +} + + + + +void LinkedModelGroup::loadValues(const QDomElement &that) +{ + foreach_model([&that](const std::string& , ModelInfo& inf) + { + // try to load, if it fails, this will load a sane initial value + inf.m_model->loadSettings(that, /*m_models()[idx].m_name*/ inf.m_name); /* TODO: m_name useful */ + }); +} + + + + +void LinkedModelGroup::addModel(AutomatableModel *model, const QString &name) +{ + model->setObjectName(name); + m_models.emplace(std::string(name.toUtf8().data()), ModelInfo(name, model)); + connect(model, &AutomatableModel::destroyed, + this, [this, model](jo_id_t){ + if(containsModel(model->objectName())) + { + emit modelRemoved(model); + eraseModel(model->objectName()); + } + }, + Qt::DirectConnection); + + // View needs to create another child view, e.g. a new knob: + emit modelAdded(model); + emit dataChanged(); +} + + + + +void LinkedModelGroup::removeControl(AutomatableModel* mdl) +{ + if(containsModel(mdl->objectName())) + { + emit modelRemoved(mdl); + eraseModel(mdl->objectName()); + } +} + + + + +bool LinkedModelGroup::eraseModel(const QString& name) +{ + return m_models.erase(name.toStdString()) > 0; +} + + + + +void LinkedModelGroup::clearModels() +{ + m_models.clear(); +} + + + + +bool LinkedModelGroup::containsModel(const QString &name) const +{ + return m_models.find(name.toStdString()) != m_models.end(); +} + + + + +/* + LinkedModelGroups +*/ + + + +LinkedModelGroups::~LinkedModelGroups() {} + + + + +void LinkedModelGroups::linkAllModels() +{ + LinkedModelGroup* first = getGroup(0); + LinkedModelGroup* cur; + + for (std::size_t i = 1; (cur = getGroup(i)); ++i) + { + first->linkControls(cur); + } +} + + + + +void LinkedModelGroups::saveSettings(QDomDocument& doc, QDomElement& that) +{ + LinkedModelGroup* grp0 = getGroup(0); + if (grp0) + { + QDomElement models = doc.createElement("models"); + that.appendChild(models); + grp0->saveValues(doc, models); + } + else { /* don't even add a "models" node */ } +} + + + + +void LinkedModelGroups::loadSettings(const QDomElement& that) +{ + QDomElement models = that.firstChildElement("models"); + LinkedModelGroup* grp0; + if (!models.isNull() && (grp0 = getGroup(0))) + { + // only load the first group, the others are linked to the first + grp0->loadValues(models); + } +} + diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 78c9f422a3f..60ab3bfbce7 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -69,6 +69,7 @@ Song::Song() : m_oldTicksPerBar( DefaultTicksPerBar ), m_masterVolumeModel( 100, 0, 200, this, tr( "Master volume" ) ), m_masterPitchModel( 0, -12, 12, this, tr( "Master pitch" ) ), + m_nLoadingTrack( 0 ), m_fileName(), m_oldFileName(), m_modified( false ), @@ -79,6 +80,7 @@ Song::Song() : m_renderBetweenMarkers( false ), m_playing( false ), m_paused( false ), + m_savingProject( false ), m_loadingProject( false ), m_isCancelled( false ), m_playMode( Mode_None ), @@ -1437,7 +1439,7 @@ QString Song::errorSummary() QString errors = m_errors.join("\n") + '\n'; errors.prepend( "\n\n" ); - errors.prepend( tr( "The following errors occured while loading: " ) ); + errors.prepend( tr( "The following errors occurred while loading: " ) ); return errors; } diff --git a/src/core/StepRecorder.cpp b/src/core/StepRecorder.cpp index 7a63e88e26e..e107c58181b 100644 --- a/src/core/StepRecorder.cpp +++ b/src/core/StepRecorder.cpp @@ -32,7 +32,8 @@ const int REMOVE_RELEASED_NOTE_TIME_THRESHOLD_MS = 70; StepRecorder::StepRecorder(PianoRoll& pianoRoll, StepRecorderWidget& stepRecorderWidget): m_pianoRoll(pianoRoll), - m_stepRecorderWidget(stepRecorderWidget) + m_stepRecorderWidget(stepRecorderWidget), + m_pattern(nullptr) { m_stepRecorderWidget.hide(); } diff --git a/src/core/fft_helpers.cpp b/src/core/fft_helpers.cpp index bc7d289e337..0dcf77e3a90 100644 --- a/src/core/fft_helpers.cpp +++ b/src/core/fft_helpers.cpp @@ -35,13 +35,12 @@ */ float maximum(const float *abs_spectrum, unsigned int spec_size) { - float maxi = 0; - unsigned int i; - if (abs_spectrum == NULL) {return -1;} - if (spec_size <= 0) {return -1;} + if (spec_size == 0) {return -1;} + + float maxi = 0; - for (i = 0; i < spec_size; i++) + for (unsigned int i = 0; i < spec_size; i++) { if (abs_spectrum[i] > maxi) {maxi = abs_spectrum[i];} } @@ -61,12 +60,11 @@ float maximum(const std::vector &abs_spectrum) */ int normalize(const float *abs_spectrum, float *norm_spectrum, unsigned int bin_count, unsigned int block_size) { - int i; - if (abs_spectrum == NULL || norm_spectrum == NULL) {return -1;} if (bin_count == 0 || block_size == 0) {return -1;} - for (i = 0; i < bin_count; i++) + block_size /= 2; + for (unsigned int i = 0; i < bin_count; i++) { norm_spectrum[i] = abs_spectrum[i] / block_size; } @@ -88,9 +86,9 @@ int normalize(const std::vector &abs_spectrum, std::vector &norm_s */ int notEmpty(const std::vector &spectrum) { - for (int i = 0; i < spectrum.size(); i++) + for (float s : spectrum) { - if (spectrum[i] != 0) {return 1;} + if (s != 0) {return 1;} } return 0; } @@ -102,22 +100,21 @@ int notEmpty(const std::vector &spectrum) */ int precomputeWindow(float *window, unsigned int length, FFT_WINDOWS type, bool normalized) { - unsigned int i; + if (window == NULL) {return -1;} + float gain = 0; float a0; float a1; float a2; float a3; - if (window == NULL) {return -1;} - // constants taken from // https://en.wikipedia.org/wiki/Window_function#AList_of_window_functions switch (type) { default: case RECTANGULAR: - for (i = 0; i < length; i++) {window[i] = 1.0;} + for (unsigned int i = 0; i < length; i++) {window[i] = 1.0;} gain = 1; return 0; case BLACKMAN_HARRIS: @@ -141,7 +138,7 @@ int precomputeWindow(float *window, unsigned int length, FFT_WINDOWS type, bool } // common computation for cosine-sum based windows - for (i = 0; i < length; i++) + for (unsigned int i = 0; i < length; i++) { window[i] = (a0 - a1 * cos(2 * F_PI * i / ((float)length - 1.0)) + a2 * cos(4 * F_PI * i / ((float)length - 1.0)) @@ -151,7 +148,7 @@ int precomputeWindow(float *window, unsigned int length, FFT_WINDOWS type, bool // apply amplitude correction gain /= (float) length; - for (i = 0; i < length; i++) {window[i] /= gain;} + for (unsigned int i = 0; i < length; i++) {window[i] /= gain;} return 0; } @@ -165,12 +162,10 @@ int precomputeWindow(float *window, unsigned int length, FFT_WINDOWS type, bool */ int absspec(const fftwf_complex *complex_buffer, float *absspec_buffer, unsigned int compl_length) { - int i; - if (complex_buffer == NULL || absspec_buffer == NULL) {return -1;} - if (compl_length <= 0) {return -1;} + if (compl_length == 0) {return -1;} - for (i = 0; i < compl_length; i++) + for (unsigned int i = 0; i < compl_length; i++) { absspec_buffer[i] = (float)sqrt(complex_buffer[i][0] * complex_buffer[i][0] + complex_buffer[i][1] * complex_buffer[i][1]); @@ -188,33 +183,28 @@ int absspec(const fftwf_complex *complex_buffer, float *absspec_buffer, unsigned */ int compressbands(const float *absspec_buffer, float *compressedband, int num_old, int num_new, int bottom, int top) { - float ratio; - int i, usefromold; - float j; - float j_min, j_max; - if (absspec_buffer == NULL || compressedband == NULL) {return -1;} if (num_old < num_new) {return -1;} if (num_old <= 0 || num_new <= 0) {return -1;} if (bottom < 0) {bottom = 0;} if (top >= num_old) {top = num_old - 1;} - usefromold = num_old - (num_old - top) - bottom; + int usefromold = num_old - (num_old - top) - bottom; - ratio = (float)usefromold / (float)num_new; + float ratio = (float)usefromold / (float)num_new; // for each new subband - for (i = 0; i < num_new; i++) + for (int i = 0; i < num_new; i++) { compressedband[i] = 0; - j_min = (i * ratio) + bottom; + float j_min = (i * ratio) + bottom; if (j_min < 0) {j_min = bottom;} - j_max = j_min + ratio; + float j_max = j_min + ratio; - for (j = (int)j_min; j <= j_max; j++) + for (float j = (int)j_min; j <= j_max; j++) { compressedband[i] += absspec_buffer[(int)j]; } @@ -222,79 +212,3 @@ int compressbands(const float *absspec_buffer, float *compressedband, int num_ol return 0; } - - -int calc13octaveband31(float *absspec_buffer, float *subbands, int num_spec, float max_frequency) -{ - static const int onethirdoctavecenterfr[] = {20, 25, 31, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 20000}; - int i, j; - float f_min, f_max, frequency, bandwidth; - int j_min, j_max = 0; - float fpower; - - if (absspec_buffer == NULL || subbands == NULL) {return -1;} - if (num_spec < 31) {return -1;} - if (max_frequency <= 0) {return -1;} - - /*** energy ***/ - fpower = 0; - for (i = 0; i < num_spec; i++) - { - absspec_buffer[i] = (absspec_buffer[i] * absspec_buffer[i]) / FFT_BUFFER_SIZE; - fpower = fpower + (2 * absspec_buffer[i]); - } - fpower = fpower - (absspec_buffer[0]); //dc not mirrored - - /*** for each subband: sum up power ***/ - for (i = 0; i < 31; i++) - { - subbands[i] = 0; - - // calculate bandwidth for subband - frequency = onethirdoctavecenterfr[i]; - - bandwidth = (pow(2, 1.0/3.0)-1) * frequency; - - f_min = frequency - bandwidth / 2.0; - f_max = frequency + bandwidth / 2.0; - - j_min = (int)(f_min / max_frequency * (float)num_spec); - j_max = (int)(f_max / max_frequency * (float)num_spec); - - - if (j_min < 0 || j_max < 0) - { - fprintf(stderr, "Error: calc13octaveband31() in fft_helpers.cpp line %d failed.\n", __LINE__); - return -1; - } - - for (j = j_min; j <= j_max; j++) - { - if (j_max < num_spec) {subbands[i] += absspec_buffer[j];} - } - - } //for - - return 0; -} - -/* Compute power of finite time sequence. - * Take care num_values is length of timesignal[] - * - * return power on success, else -1 - */ -float signalpower(const float *timesignal, int num_values) -{ - if (num_values <= 0) {return -1;} - - if (timesignal == NULL) {return -1;} - - float power = 0; - for (int i = 0; i < num_values; i++) - { - power += timesignal[i] * timesignal[i]; - } - - return power; -} - diff --git a/src/core/main.cpp b/src/core/main.cpp index 531a0a4da89..26d12713c8a 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -894,7 +894,7 @@ int main( int argc, char * * argv ) } else // Exit { - return 0; + return EXIT_SUCCESS; } } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index af316fddb01..dbdcc5d84da 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -52,6 +52,7 @@ SET(LMMS_SRCS gui/widgets/ComboBox.cpp gui/widgets/ControllerRackView.cpp gui/widgets/ControllerView.cpp + gui/widgets/Controls.cpp gui/widgets/CPULoadWidget.cpp gui/widgets/EffectRackView.cpp gui/widgets/EffectView.cpp @@ -71,6 +72,8 @@ SET(LMMS_SRCS gui/widgets/LcdSpinBox.cpp gui/widgets/LcdWidget.cpp gui/widgets/LedCheckbox.cpp + gui/widgets/ControlLayout.cpp + gui/widgets/LinkedModelGroupViews.cpp gui/widgets/MeterDialog.cpp gui/widgets/MidiPortMenu.cpp gui/widgets/NStateButton.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 7eeb87ed29c..5abbdf82f25 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -444,8 +444,7 @@ void FileBrowserTreeWidget::mousePressEvent(QMouseEvent * me ) if( !dataFile.validate( f->extension() ) ) { QMessageBox::warning( 0, tr ( "Error" ), - f->fullName() + " " + tr( "does not appear to be a valid" ) + " " + f->extension() + - " " + tr( "file" ), + tr( "%1 does not appear to be a valid %2 file" ).arg( f->fullName(), f->extension() ), QMessageBox::Ok, QMessageBox::NoButton ); m_pphMutex.unlock(); return; diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 3b42bd9b267..5918c9333bb 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -152,6 +152,8 @@ PianoRoll::PianoRoll() : m_zoomingModel(), m_quantizeModel(), m_noteLenModel(), + m_scaleModel(), + m_chordModel(), m_pattern( NULL ), m_currentPosition(), m_recording( false ), diff --git a/src/gui/widgets/ControlLayout.cpp b/src/gui/widgets/ControlLayout.cpp new file mode 100644 index 00000000000..afd4e68e42e --- /dev/null +++ b/src/gui/widgets/ControlLayout.cpp @@ -0,0 +1,308 @@ +/* + * ControlLayout.cpp - implementation for ControlLayout.h + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include "ControlLayout.h" + +#include +#include +#include +#include +#include + +constexpr const int ControlLayout::m_minWidth; + +ControlLayout::ControlLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) + : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing), + m_searchBar(new QLineEdit(parent)) +{ + setContentsMargins(margin, margin, margin, margin); + m_searchBar->setPlaceholderText("filter"); + m_searchBar->setObjectName(s_searchBarName); + connect(m_searchBar, SIGNAL(textChanged(const QString&)), + this, SLOT(onTextChanged(const QString& ))); + addWidget(m_searchBar); + m_searchBar->setHidden(true); // nothing to filter yet +} + +ControlLayout::~ControlLayout() +{ + QLayoutItem *item; + while ((item = takeAt(0))) { delete item; } +} + +void ControlLayout::onTextChanged(const QString&) +{ + invalidate(); + update(); +} + +void ControlLayout::addItem(QLayoutItem *item) +{ + QWidget* widget = item->widget(); + const QString str = widget ? widget->objectName() : QString("unnamed"); + m_itemMap.insert(str, item); + invalidate(); +} + +int ControlLayout::horizontalSpacing() const +{ + if (m_hSpace >= 0) { return m_hSpace; } + else + { + return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); + } +} + +int ControlLayout::verticalSpacing() const +{ + if (m_vSpace >= 0) { return m_vSpace; } + else + { + return smartSpacing(QStyle::PM_LayoutVerticalSpacing); + } +} + +int ControlLayout::count() const +{ + return m_itemMap.size() - 1; +} + +QMap::const_iterator +ControlLayout::pairAt(int index) const +{ + if (index < 0) { return m_itemMap.cend(); } + + auto skip = [&](QLayoutItem* item) -> bool + { + return item->widget()->objectName() == s_searchBarName; + }; + + QMap::const_iterator itr = m_itemMap.cbegin(); + for (; itr != m_itemMap.cend() && (index > 0 || skip(itr.value())); ++itr) + { + if(!skip(itr.value())) { index--; } + } + return itr; +} + +// linear time :-( +QLayoutItem *ControlLayout::itemAt(int index) const +{ + auto itr = pairAt(index); + return (itr == m_itemMap.end()) ? nullptr : itr.value(); +} + +QLayoutItem *ControlLayout::itemByString(const QString &key) const +{ + auto itr = m_itemMap.find(key); + return (itr == m_itemMap.end()) ? nullptr : *itr; +} + +// linear time :-( +QLayoutItem *ControlLayout::takeAt(int index) +{ + auto itr = pairAt(index); + return (itr == m_itemMap.end()) ? nullptr : m_itemMap.take(itr.key()); +} + +Qt::Orientations ControlLayout::expandingDirections() const +{ + return Qt::Orientations(); +} + +bool ControlLayout::hasHeightForWidth() const +{ + return true; +} + +int ControlLayout::heightForWidth(int width) const +{ + int height = doLayout(QRect(0, 0, width, 0), true); + return height; +} + +void ControlLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + doLayout(rect, false); +} + +QSize ControlLayout::sizeHint() const +{ + return minimumSize(); +} + +QSize ControlLayout::minimumSize() const +{ + // original formula from Qt's FlowLayout example: + // get maximum height and width for all children. + // as Qt will later call heightForWidth, only the width here really matters + QSize size; + for (const QLayoutItem *item : qAsConst(m_itemMap)) + { + size = size.expandedTo(item->minimumSize()); + } + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom()); + + // the original formula would leed to ~1 widget per row + // bash it at least to 400 so we have ~4 knobs per row + size.setWidth(qMax(size.width(), m_minWidth)); + return size; +} + +int ControlLayout::doLayout(const QRect &rect, bool testOnly) const +{ + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + + const QString filterText = m_searchBar->text(); + bool first = true; + + QMapIterator itr(m_itemMap); + while (itr.hasNext()) + { + itr.next(); + QLayoutItem* item = itr.value(); + QWidget *wid = item->widget(); + if (wid) + { + if ( first || // do not filter search bar + filterText.isEmpty() || // no filter - pass all + itr.key().contains(filterText, Qt::CaseInsensitive)) + { + if (first) + { + // for the search bar, only show it if there are at least + // two control widgets (i.e. at least 3 widgets) + if (m_itemMap.size() > 2) { wid->show(); } + else { wid->hide(); } + } + else { wid->show(); } + + int spaceX = horizontalSpacing(); + if (spaceX == -1) + { + spaceX = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal); + } + int spaceY = verticalSpacing(); + if (spaceY == -1) + { + spaceY = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical); + } + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) + { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) + { + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + } + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + first = false; + } + else + { + wid->hide(); + } + } + } + return y + lineHeight - rect.y() + bottom; +} + +int ControlLayout::smartSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) { return -1; } + else if (parent->isWidgetType()) + { + QWidget *pw = static_cast(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } + else { return static_cast(parent)->spacing(); } +} + + diff --git a/src/gui/widgets/Controls.cpp b/src/gui/widgets/Controls.cpp new file mode 100644 index 00000000000..15b4e0d282a --- /dev/null +++ b/src/gui/widgets/Controls.cpp @@ -0,0 +1,140 @@ +/* + * Controls.cpp - labeled control widgets + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Controls.h" + +#include +#include +#include + +#include "ComboBox.h" +#include "LcdSpinBox.h" +#include "LedCheckbox.h" +#include "Knob.h" + + + + +Control::~Control() {} + + + + +void KnobControl::setText(const QString &text) { m_knob->setLabel(text); } + +QWidget *KnobControl::topWidget() { return m_knob; } + +void KnobControl::setModel(AutomatableModel *model) +{ + m_knob->setModel(model->dynamicCast(true)); +} + +FloatModel *KnobControl::model() { return m_knob->model(); } + +AutomatableModelView* KnobControl::modelView() { return m_knob; } + +KnobControl::KnobControl(QWidget *parent) : + m_knob(new Knob(parent)) {} + +KnobControl::~KnobControl() {} + + + + +void ComboControl::setText(const QString &text) { m_label->setText(text); } + +void ComboControl::setModel(AutomatableModel *model) +{ + m_combo->setModel(model->dynamicCast(true)); +} + +ComboBoxModel *ComboControl::model() { return m_combo->model(); } + +AutomatableModelView* ComboControl::modelView() { return m_combo; } + +ComboControl::ComboControl(QWidget *parent) : + m_widget(new QWidget(parent)), + m_combo(new ComboBox(nullptr)), + m_label(new QLabel(m_widget)) +{ + m_combo->setFixedSize(64, 22); + QVBoxLayout* vbox = new QVBoxLayout(m_widget); + vbox->addWidget(m_combo); + vbox->addWidget(m_label); + m_combo->repaint(); +} + +ComboControl::~ComboControl() {} + + + + +void CheckControl::setText(const QString &text) { m_label->setText(text); } + +QWidget *CheckControl::topWidget() { return m_widget; } + +void CheckControl::setModel(AutomatableModel *model) +{ + m_checkBox->setModel(model->dynamicCast(true)); +} + +BoolModel *CheckControl::model() { return m_checkBox->model(); } + +AutomatableModelView* CheckControl::modelView() { return m_checkBox; } + +CheckControl::CheckControl(QWidget *parent) : + m_widget(new QWidget(parent)), + m_checkBox(new LedCheckBox(nullptr, QString(), LedCheckBox::Green)), + m_label(new QLabel(m_widget)) +{ + QVBoxLayout* vbox = new QVBoxLayout(m_widget); + vbox->addWidget(m_checkBox); + vbox->addWidget(m_label); +} + +CheckControl::~CheckControl() {} + + + + +void LcdControl::setText(const QString &text) { m_lcd->setLabel(text); } + +QWidget *LcdControl::topWidget() { return m_lcd; } + +void LcdControl::setModel(AutomatableModel *model) +{ + m_lcd->setModel(model->dynamicCast(true)); +} + +IntModel *LcdControl::model() { return m_lcd->model(); } + +AutomatableModelView* LcdControl::modelView() { return m_lcd; } + +LcdControl::LcdControl(int numDigits, QWidget *parent) : + m_lcd(new LcdSpinBox(numDigits, parent)) +{ +} + +LcdControl::~LcdControl() {} + diff --git a/src/gui/widgets/FadeButton.cpp b/src/gui/widgets/FadeButton.cpp index b633286b18b..8f75ea33be0 100644 --- a/src/gui/widgets/FadeButton.cpp +++ b/src/gui/widgets/FadeButton.cpp @@ -24,12 +24,10 @@ #include -#include #include #include "embed.h" #include "FadeButton.h" -#include "update_event.h" const float FadeDuration = 300; @@ -71,7 +69,7 @@ void FadeButton::activate() { m_stateTimer.restart(); activeNotes++; - signalUpdate(); + update(); } @@ -94,14 +92,6 @@ void FadeButton::noteEnd() m_releaseTimer.restart(); } - signalUpdate(); -} - - - - -void FadeButton::customEvent(QEvent *) -{ update(); } @@ -165,9 +155,3 @@ QColor FadeButton::fadeToColor(QColor startCol, QColor endCol, QTime timer, floa return col; } - - -void FadeButton::signalUpdate() -{ - QApplication::postEvent(this, new updateEvent()); -} diff --git a/src/gui/widgets/LinkedModelGroupViews.cpp b/src/gui/widgets/LinkedModelGroupViews.cpp new file mode 100644 index 00000000000..21d40efcc1e --- /dev/null +++ b/src/gui/widgets/LinkedModelGroupViews.cpp @@ -0,0 +1,160 @@ +/* + * LinkedModelGroupViews.h - view for groups of linkable models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "LinkedModelGroupViews.h" + +#include +#include "Controls.h" +#include "ControlLayout.h" +#include "LinkedModelGroups.h" + + +/* + LinkedModelGroupViewBase +*/ + + +LinkedModelGroupView::LinkedModelGroupView(QWidget* parent, + LinkedModelGroup *model, std::size_t colNum) : + QWidget(parent), + m_model(model), + m_colNum(colNum), + m_layout(new ControlLayout(this)) +{ +} + + + + +LinkedModelGroupView::~LinkedModelGroupView() {} + + + + +void LinkedModelGroupView::modelChanged(LinkedModelGroup *group) +{ + // reconnect models + group->foreach_model([this](const std::string& str, + const LinkedModelGroup::ModelInfo& minf) + { + auto itr = m_widgets.find(str); + // in case there are new or deleted widgets, the subclass has already + // modified m_widgets, so this will go into the else case + if (itr == m_widgets.end()) + { + // no widget? this can happen when the whole view is being destroyed + // (for some strange reasons) + } + else + { + itr->second->setModel(minf.m_model); + } + }); + + m_model = group; +} + + + + +void LinkedModelGroupView::addControl(Control* ctrl, const std::string& id, + const std::string &display, bool removable) +{ + int wdgNum = static_cast(m_widgets.size()); + if (ctrl) + { + QWidget* box = new QWidget(this); + QHBoxLayout* boxLayout = new QHBoxLayout(box); + boxLayout->addWidget(ctrl->topWidget()); + + if (removable) + { + QPushButton* removeBtn = new QPushButton; + removeBtn->setIcon( embed::getIconPixmap( "discard" ) ); + QObject::connect(removeBtn, &QPushButton::clicked, + this, [this,ctrl](bool){ + AutomatableModel* controlModel = ctrl->model(); + // remove control out of model group + // (will also remove it from the UI) + m_model->removeControl(controlModel); + // delete model (includes disconnecting all connections) + delete controlModel; + }, + Qt::DirectConnection); + boxLayout->addWidget(removeBtn); + } + + // required, so the Layout knows how to sort/filter widgets by string + box->setObjectName(QString::fromStdString(display)); + m_layout->addWidget(box); + + // take ownership of control and add it + m_widgets.emplace(id, std::unique_ptr(ctrl)); + ++wdgNum; + } + + if (isHidden()) { setHidden(false); } +} + + + + +void LinkedModelGroupView::removeControl(const QString& key) +{ + auto itr = m_widgets.find(key.toStdString()); + if (itr != m_widgets.end()) + { + QLayoutItem* item = m_layout->itemByString(key); + Q_ASSERT(!!item); + QWidget* wdg = item->widget(); + Q_ASSERT(!!wdg); + + // remove item from layout + m_layout->removeItem(item); + // the widget still exists and is visible - remove it now + delete wdg; + // erase widget pointer from dictionary + m_widgets.erase(itr); + // repaint immediately, so we don't have dangling model views + m_layout->update(); + } +} + + +/* + LinkedModelGroupsViewBase +*/ + + +void LinkedModelGroupsView::modelChanged(LinkedModelGroups *groups) +{ + LinkedModelGroupView* groupView = getGroupView(); + LinkedModelGroup* group0 = groups->getGroup(0); + if (group0 && groupView) + { + groupView->modelChanged(group0); + } +} + +