diff --git a/.gitmodules b/.gitmodules index ee6e7eac9d8..fa6980ac5f8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,9 +40,9 @@ [submodule "plugins/CarlaBase/carla"] path = plugins/CarlaBase/carla url = https://github.com/falktx/carla -[submodule "plugins/Sid/resid"] - path = plugins/Sid/resid - url = https://github.com/simonowen/resid +[submodule "plugins/Sid/resid/resid"] + path = plugins/Sid/resid/resid + url = https://github.com/libsidplayfp/resid [submodule "src/3rdparty/jack2"] path = src/3rdparty/jack2 url = https://github.com/jackaudio/jack2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 5db72ef830c..27333dfbdfd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ IF(COMMAND CMAKE_POLICY) CMAKE_POLICY(SET CMP0057 NEW) # TODO: Keep CMP0074 but remove this condition when cmake 3.12+ is guaranteed IF(${CMAKE_VERSION} VERSION_GREATER_EQUAL 3.12) + # Needed for the SWH Ladspa plugins. See below. CMAKE_POLICY(SET CMP0074 NEW) # find_package() uses _ROOT variables ENDIF() ENDIF(COMMAND CMAKE_POLICY) @@ -85,6 +86,7 @@ OPTION(WANT_SOUNDIO "Include libsoundio support" ON) OPTION(WANT_SDL "Include SDL (Simple DirectMedia Layer) support" ON) OPTION(WANT_SF2 "Include SoundFont2 player plugin" ON) OPTION(WANT_GIG "Include GIG player plugin" ON) +option(WANT_SID "Include Sid instrument" ON) OPTION(WANT_STK "Include Stk (Synthesis Toolkit) support" ON) OPTION(WANT_SWH "Include Steve Harris's LADSPA plugins" ON) OPTION(WANT_TAP "Include Tom's Audio Processing LADSPA plugins" ON) @@ -93,6 +95,10 @@ OPTION(WANT_VST_32 "Include 32-bit VST support" ON) OPTION(WANT_VST_64 "Include 64-bit VST support" ON) OPTION(WANT_WINMM "Include WinMM MIDI support" OFF) OPTION(WANT_DEBUG_FPE "Debug floating point exceptions" OFF) +option(WANT_DEBUG_ASAN "Enable AddressSanitizer" OFF) +option(WANT_DEBUG_TSAN "Enable ThreadSanitizer" OFF) +option(WANT_DEBUG_MSAN "Enable MemorySanitizer" OFF) +option(WANT_DEBUG_UBSAN "Enable UndefinedBehaviorSanitizer" OFF) OPTION(BUNDLE_QT_TRANSLATIONS "Install Qt translation files for LMMS" OFF) @@ -215,6 +221,13 @@ CHECK_CXX_SOURCE_COMPILES( LMMS_HAVE_SF_COMPLEVEL ) +# check for perl +if(LMMS_BUILD_APPLE) + # Prefer system perl over Homebrew, MacPorts, etc + set(Perl_ROOT "/usr/bin") +endif() +find_package(Perl) + IF(WANT_LV2) IF(PKG_CONFIG_FOUND) PKG_CHECK_MODULES(LV2 lv2) @@ -353,6 +366,16 @@ IF(WANT_SDL AND NOT LMMS_HAVE_SDL2) ENDIF() ENDIF() +# check for Sid +if(WANT_SID) + if(PERL_FOUND) + set(LMMS_HAVE_SID TRUE) + set(STATUS_SID "OK") + else() + set(STATUS_SID "not found, please install perl if you require the Sid instrument") + endif() +endif() + # check for Stk IF(WANT_STK) FIND_PACKAGE(STK) @@ -518,7 +541,11 @@ IF(WANT_SF2) find_package(FluidSynth 1.1.0) if(FluidSynth_FOUND) SET(LMMS_HAVE_FLUIDSYNTH TRUE) - SET(STATUS_FLUIDSYNTH "OK") + if(FluidSynth_VERSION_STRING VERSION_GREATER_EQUAL 2) + set(STATUS_FLUIDSYNTH "OK") + else() + set(STATUS_FLUIDSYNTH "OK (FluidSynth version < 2: per-note panning unsupported)") + endif() else() SET(STATUS_FLUIDSYNTH "not found, libfluidsynth-dev (or similar)" "is highly recommended") @@ -629,7 +656,9 @@ else() set(NOOP_COMMAND "${CMAKE_COMMAND}" "-E" "echo") endif() if(STRIP) - set(STRIP_COMMAND "$,${NOOP_COMMAND},${STRIP}>") + # TODO CMake 3.19: Now that CONFIG generator expressions support testing for + # multiple configurations, combine the OR into a single CONFIG expression. + set(STRIP_COMMAND "$,$>,${NOOP_COMMAND},${STRIP}>") else() set(STRIP_COMMAND "${NOOP_COMMAND}") endif() @@ -665,8 +694,36 @@ IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") ELSE(WIN32) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -DPIC") ENDIF(WIN32) +elseif(MSVC) + # Use UTF-8 as the source and execution character set + add_compile_options("/utf-8") ENDIF() +# add enabled sanitizers +function(add_sanitizer sanitizer supported_compilers want_flag status_flag) + if(${want_flag}) + if(CMAKE_CXX_COMPILER_ID MATCHES "${supported_compilers}") + set("${status_flag}" "Enabled" PARENT_SCOPE) + string(REPLACE ";" " " additional_flags "${ARGN}") + # todo CMake 3.13: use add_compile_options/add_link_options instead + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=${sanitizer} ${additional_flags}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=${sanitizer} ${additional_flags}" PARENT_SCOPE) + else() + set("${status_flag}" "Wanted but disabled due to unsupported compiler" PARENT_SCOPE) + endif() + else() + set("${status_flag}" "Disabled" PARENT_SCOPE) + endif() +endfunction() + +add_sanitizer(address "GNU|Clang|MSVC" WANT_DEBUG_ASAN STATUS_DEBUG_ASAN) +add_sanitizer(thread "GNU|Clang" WANT_DEBUG_TSAN STATUS_DEBUG_TSAN) +add_sanitizer(memory "Clang" WANT_DEBUG_MSAN STATUS_DEBUG_MSAN -fno-omit-frame-pointer) +# UBSan does not link with vptr enabled due to a problem with references from PeakControllerEffect +# not being found by PeakController +add_sanitizer(undefined "GNU|Clang" WANT_DEBUG_UBSAN STATUS_DEBUG_UBSAN -fno-sanitize=vptr) + + # use ccache include(CompileCache) @@ -735,7 +792,6 @@ ADD_CUSTOM_TARGET(uninstall COMMAND ${CMAKE_COMMAND} -DCMAKE_INSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/uninstall.cmake" ) - # # display configuration information # @@ -787,6 +843,7 @@ MESSAGE( "* ZynAddSubFX instrument : ${STATUS_ZYN}\n" "* Carla Patchbay & Rack : ${STATUS_CARLA}\n" "* SoundFont2 player : ${STATUS_FLUIDSYNTH}\n" +"* Sid instrument : ${STATUS_SID}\n" "* Stk Mallets : ${STATUS_STK}\n" "* VST-instrument hoster : ${STATUS_VST}\n" "* VST-effect hoster : ${STATUS_VST}\n" @@ -801,7 +858,11 @@ MESSAGE( MESSAGE( "Developer options\n" "-----------------------------------------\n" -"* Debug FP exceptions : ${STATUS_DEBUG_FPE}\n" +"* Debug FP exceptions : ${STATUS_DEBUG_FPE}\n" +"* Debug using AddressSanitizer : ${STATUS_DEBUG_ASAN}\n" +"* Debug using ThreadSanitizer : ${STATUS_DEBUG_TSAN}\n" +"* Debug using MemorySanitizer : ${STATUS_DEBUG_MSAN}\n" +"* Debug using UBSanitizer : ${STATUS_DEBUG_UBSAN}\n" ) MESSAGE( diff --git a/data/locale/ar.ts b/data/locale/ar.ts index 1f159c42a2a..0d44c22bf5c 100644 --- a/data/locale/ar.ts +++ b/data/locale/ar.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/bs.ts b/data/locale/bs.ts index 506b401bd61..7abf0baf1e1 100644 --- a/data/locale/bs.ts +++ b/data/locale/bs.ts @@ -3677,7 +3677,7 @@ You can remove and move mixer channels in the context menu, which is accessed by - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ca.ts b/data/locale/ca.ts index 765cf3b6081..0e27c39db80 100644 --- a/data/locale/ca.ts +++ b/data/locale/ca.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/cs.ts b/data/locale/cs.ts index 0ed175022be..022f554592a 100644 --- a/data/locale/cs.ts +++ b/data/locale/cs.ts @@ -6361,7 +6361,7 @@ Ověřte si prosím, zda máte povolen zápis do souboru a do složky, ve které - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/de.ts b/data/locale/de.ts index 51ca7d56204..7817857fdff 100644 --- a/data/locale/de.ts +++ b/data/locale/de.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/el.ts b/data/locale/el.ts index 320a6657f61..07e61778f1e 100644 --- a/data/locale/el.ts +++ b/data/locale/el.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/en.ts b/data/locale/en.ts index e52ae39ab7e..15c3ab1f07c 100644 --- a/data/locale/en.ts +++ b/data/locale/en.ts @@ -6362,7 +6362,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/eo.ts b/data/locale/eo.ts index 005ee8100bf..0dd9c405f67 100644 --- a/data/locale/eo.ts +++ b/data/locale/eo.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/es.ts b/data/locale/es.ts index 4fc4951ef92..3953ddc11bf 100644 --- a/data/locale/es.ts +++ b/data/locale/es.ts @@ -6361,7 +6361,7 @@ Asegúrate de tener permisos de escritura tanto del archivo como del directorio - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/eu.ts b/data/locale/eu.ts index 25c165f81f8..fe6495c0a65 100644 --- a/data/locale/eu.ts +++ b/data/locale/eu.ts @@ -6641,7 +6641,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/fa.ts b/data/locale/fa.ts index 181ca0ca1ff..b376a8424f8 100644 --- a/data/locale/fa.ts +++ b/data/locale/fa.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/fr.ts b/data/locale/fr.ts index 2c65444a8e0..4862f4263ce 100644 --- a/data/locale/fr.ts +++ b/data/locale/fr.ts @@ -6645,7 +6645,7 @@ Veuillez vous assurez que vous avez les droits d'écriture sur le fichier e - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/gl.ts b/data/locale/gl.ts index cf04fd5d428..a1a9e6bf1a4 100644 --- a/data/locale/gl.ts +++ b/data/locale/gl.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/he.ts b/data/locale/he.ts index ee5a23613b2..fef0caa9178 100644 --- a/data/locale/he.ts +++ b/data/locale/he.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/hi_IN.ts b/data/locale/hi_IN.ts index 15550231f85..82cf364e332 100644 --- a/data/locale/hi_IN.ts +++ b/data/locale/hi_IN.ts @@ -6362,7 +6362,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/hu_HU.ts b/data/locale/hu_HU.ts index 83605994674..a0f1e4d4542 100644 --- a/data/locale/hu_HU.ts +++ b/data/locale/hu_HU.ts @@ -6366,7 +6366,7 @@ Ellenőrizd, hogy rendelkezel-e a szükséges engedélyekkel és próbáld újra - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/id.ts b/data/locale/id.ts index e381ea726d8..c504740e936 100644 --- a/data/locale/id.ts +++ b/data/locale/id.ts @@ -6362,7 +6362,7 @@ Pastikan Anda memiliki izin menulis ke file dan direktori yang berisi berkas ter - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/it.ts b/data/locale/it.ts index ff146d47120..d5a68e6e7c3 100644 --- a/data/locale/it.ts +++ b/data/locale/it.ts @@ -6366,7 +6366,7 @@ Si prega di controllare i permessi di scrittura sul file e la cartella che lo co - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ja.ts b/data/locale/ja.ts index e10ca511818..14b38c6984f 100644 --- a/data/locale/ja.ts +++ b/data/locale/ja.ts @@ -6362,7 +6362,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ka.ts b/data/locale/ka.ts index 1956d8d04e5..51eededf26b 100644 --- a/data/locale/ka.ts +++ b/data/locale/ka.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ko.ts b/data/locale/ko.ts index 7373b5ca9a7..43b99e7f437 100644 --- a/data/locale/ko.ts +++ b/data/locale/ko.ts @@ -6364,7 +6364,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ms_MY.ts b/data/locale/ms_MY.ts index 209d51d108c..ff34784219a 100644 --- a/data/locale/ms_MY.ts +++ b/data/locale/ms_MY.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/nb.ts b/data/locale/nb.ts index 3675b7f589c..659344d64de 100644 --- a/data/locale/nb.ts +++ b/data/locale/nb.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/nl.ts b/data/locale/nl.ts index ad630a249c9..7ff3e8735a8 100644 --- a/data/locale/nl.ts +++ b/data/locale/nl.ts @@ -6362,7 +6362,7 @@ Zorg ervoor dat u schrijfbevoegdheid heeft voor het bestand en voor de map die h - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/oc.ts b/data/locale/oc.ts index 58c81c96453..045eaf3ad69 100644 --- a/data/locale/oc.ts +++ b/data/locale/oc.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/pl.ts b/data/locale/pl.ts index bb0c64edec0..ff36a8daca6 100644 --- a/data/locale/pl.ts +++ b/data/locale/pl.ts @@ -6646,7 +6646,7 @@ Upewnij się, że masz uprawnienia do zapisu do pliku i katalogu zawierającego - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/pt.ts b/data/locale/pt.ts index b375e289f38..f8cfe76181c 100644 --- a/data/locale/pt.ts +++ b/data/locale/pt.ts @@ -6363,7 +6363,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ro.ts b/data/locale/ro.ts index eceb45a6485..58abbba9959 100644 --- a/data/locale/ro.ts +++ b/data/locale/ro.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ru.ts b/data/locale/ru.ts index 8235f291f34..73b7e06ad2e 100644 --- a/data/locale/ru.ts +++ b/data/locale/ru.ts @@ -6375,7 +6375,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/sl.ts b/data/locale/sl.ts index 3ad55a4c044..e7bfbc3081c 100644 --- a/data/locale/sl.ts +++ b/data/locale/sl.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/sr.ts b/data/locale/sr.ts index 9b90164ab3b..183936bc74c 100644 --- a/data/locale/sr.ts +++ b/data/locale/sr.ts @@ -2956,7 +2956,7 @@ You can remove and move mixer channels in the context menu, which is accessed by - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/sv.ts b/data/locale/sv.ts index 4963b07a9ad..f5d4e0fb496 100644 --- a/data/locale/sv.ts +++ b/data/locale/sv.ts @@ -6644,7 +6644,7 @@ Se till att du har skrivbehörighet till filen och mappen som innehåller filen - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/tr.ts b/data/locale/tr.ts index 387be6d8b96..b899337a543 100644 --- a/data/locale/tr.ts +++ b/data/locale/tr.ts @@ -6646,7 +6646,7 @@ Lütfen dosyaya ve dosyayı içeren dizine yazma izniniz olduğundan emin olun v - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/uk.ts b/data/locale/uk.ts index 50df10e4b72..9fb6389c956 100644 --- a/data/locale/uk.ts +++ b/data/locale/uk.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/zh_CN.ts b/data/locale/zh_CN.ts index 63b22df9902..9b783b963dd 100644 --- a/data/locale/zh_CN.ts +++ b/data/locale/zh_CN.ts @@ -6370,7 +6370,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/zh_TW.ts b/data/locale/zh_TW.ts index 791a45599f9..a3a727edb0b 100644 --- a/data/locale/zh_TW.ts +++ b/data/locale/zh_TW.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/samples/drums/kick04.ogg b/data/samples/drums/kick04.ogg index 567480abd68..8f7dce52712 100644 Binary files a/data/samples/drums/kick04.ogg and b/data/samples/drums/kick04.ogg differ diff --git a/data/samples/effects/scratch01.ogg b/data/samples/effects/scratch01.ogg index 9f216038dc4..0b05505cd36 100644 Binary files a/data/samples/effects/scratch01.ogg and b/data/samples/effects/scratch01.ogg differ diff --git a/data/samples/effects/wind_chimes01.ogg b/data/samples/effects/wind_chimes01.ogg index 35d3374a2e2..7fb3c441a24 100644 Binary files a/data/samples/effects/wind_chimes01.ogg and b/data/samples/effects/wind_chimes01.ogg differ diff --git a/data/samples/instruments/harpsichord01.ogg b/data/samples/instruments/harpsichord01.ogg index 028bbd91259..c84ffd7df44 100644 Binary files a/data/samples/instruments/harpsichord01.ogg and b/data/samples/instruments/harpsichord01.ogg differ diff --git a/data/samples/misc/hit01.ogg b/data/samples/misc/hit01.ogg index d5e93633e99..30dd8613579 100644 Binary files a/data/samples/misc/hit01.ogg and b/data/samples/misc/hit01.ogg differ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index d1f4d0588a9..c73da5a2b58 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -82,7 +82,6 @@ lmms--gui--TextFloat, lmms--gui--SimpleTextFloat { QMenu { border:1px solid #747474; background-color: #c9c9c9; - font-size:11px; } QMenu::separator { @@ -98,15 +97,12 @@ QMenu::item { QMenu::item:selected { color: white; - font-weight:bold; background-color: #747474; } QMenu::item:disabled { color: #747474; background-color: #c9c9c9; - font-size:12px; - font-weight: normal; padding: 4px 32px 4px 20px; } @@ -132,7 +128,7 @@ QMenu::indicator:selected { lmms--gui--FileBrowser QCheckBox { - font-size: 10px; + font-size: 8pt; color: white; } @@ -209,6 +205,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; background: url(resources:cpuload_bg.png); + qproperty-stepSize: 4; } /* scrollbar: trough */ diff --git a/data/themes/default/edit_draw_small.png b/data/themes/default/edit_draw_small.png new file mode 100644 index 00000000000..9979c822345 Binary files /dev/null and b/data/themes/default/edit_draw_small.png differ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index e88e51e5bd4..854d4a4c39e 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -9,7 +9,6 @@ QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar { QTreeView { outline: none; - font-size: 12px; } QTreeWidget::item { @@ -42,7 +41,7 @@ QMdiArea { lmms--gui--FileBrowser QCheckBox { - font-size: 10px; + font-size: 8pt; color: white; } @@ -115,7 +114,6 @@ QSplashScreen QLabel { QMenu { border-top: 2px solid #08993E; background-color: #15191c; - font-size: 11px; } QMenu::separator { @@ -133,15 +131,12 @@ QMenu::item { QMenu::item:selected { color: #d1d8e4; - font-weight: normal; background-color: #21272b; } QMenu::item:disabled { color: #515459; background-color: #262b30; - font-size: 12px; - font-weight: normal; padding: 4px 32px 4px 20px; } @@ -241,6 +236,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; background: url(resources:cpuload_bg.png); + qproperty-stepSize: 1; } /* scrollbar: trough */ diff --git a/data/themes/default/tuning_tab.png b/data/themes/default/tuning_tab.png new file mode 100644 index 00000000000..41c4f2d9fde Binary files /dev/null and b/data/themes/default/tuning_tab.png differ diff --git a/include/ArrayVector.h b/include/ArrayVector.h new file mode 100644 index 00000000000..06e09226cf7 --- /dev/null +++ b/include/ArrayVector.h @@ -0,0 +1,388 @@ +/* + * ArrayVector.h + * + * Copyright (c) 2023 Dominic Clark + * + * 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 LMMS_ARRAY_VECTOR_H +#define LMMS_ARRAY_VECTOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lmms { + +namespace detail { + +template +constexpr bool is_input_iterator_v = false; + +template +constexpr bool is_input_iterator_v::iterator_category>> = + std::is_convertible_v::iterator_category, std::input_iterator_tag>; + +} // namespace detail + +/** + * A container that stores up to a maximum of `N` elements of type `T` directly + * within itself, rather than separately on the heap. Useful when a dynamically + * resizeable container is needed for use in real-time code. Can be thought of + * as a hybrid between `std::array` and `std::vector`. The interface follows + * that of `std::vector` - see standard C++ documentation. + */ +template +class ArrayVector +{ +public: + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using value_type = T; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = pointer; + using const_iterator = const_pointer; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + ArrayVector() = default; + + ArrayVector(const ArrayVector& other) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{other.m_size} + { + std::uninitialized_copy(other.begin(), other.end(), begin()); + } + + ArrayVector(ArrayVector&& other) noexcept(std::is_nothrow_move_constructible_v) : + m_size{other.m_size} + { + std::uninitialized_move(other.begin(), other.end(), begin()); + other.clear(); + } + + ArrayVector(size_type count, const T& value) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{count} + { + assert(count <= N); + std::uninitialized_fill_n(begin(), count, value); + } + + explicit ArrayVector(size_type count) noexcept(std::is_nothrow_default_constructible_v) : + m_size{count} + { + assert(count <= N); + std::uninitialized_value_construct_n(begin(), count); + } + + template, int> = 0> + ArrayVector(It first, It last) + { + // Can't check the size first as the iterator may not be multipass + const auto end = std::uninitialized_copy(first, last, begin()); + m_size = end - begin(); + assert(m_size <= N); + } + + ArrayVector(std::initializer_list il) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{il.size()} + { + assert(il.size() <= N); + std::uninitialized_copy(il.begin(), il.end(), begin()); + } + + ~ArrayVector() { std::destroy(begin(), end()); } + + ArrayVector& operator=(const ArrayVector& other) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + if (this != &other) { + const auto toAssign = std::min(other.size(), size()); + const auto assignedFromEnd = other.begin() + toAssign; + const auto assignedToEnd = std::copy(other.begin(), other.begin() + toAssign, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_copy(assignedFromEnd, other.end(), end()); + m_size = other.size(); + } + return *this; + } + + ArrayVector& operator=(ArrayVector&& other) + noexcept(std::is_nothrow_move_assignable_v && std::is_nothrow_move_constructible_v) + { + if (this != &other) { + const auto toAssign = std::min(other.size(), size()); + const auto assignedFromEnd = other.begin() + toAssign; + const auto assignedToEnd = std::move(other.begin(), other.begin() + toAssign, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_move(assignedFromEnd, other.end(), end()); + m_size = other.size(); + other.clear(); + } + return *this; + } + + ArrayVector& operator=(std::initializer_list il) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + assert(il.size() <= N); + const auto toAssign = std::min(il.size(), size()); + const auto assignedFromEnd = il.begin() + toAssign; + const auto assignedToEnd = std::copy(il.begin(), assignedFromEnd, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_copy(assignedFromEnd, il.end(), end()); + m_size = il.size(); + return *this; + } + + void assign(size_type count, const T& value) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + assert(count <= N); + const auto temp = value; + const auto toAssign = std::min(count, size()); + const auto toConstruct = count - toAssign; + const auto assignedToEnd = std::fill_n(begin(), toAssign, temp); + std::destroy(assignedToEnd, end()); + std::uninitialized_fill_n(assignedToEnd, toConstruct, temp); + m_size = count; + } + + template, int> = 0> + void assign(It first, It last) + { + // Can't check the size first as the iterator may not be multipass + auto pos = begin(); + for (; first != last && pos != end(); ++pos, ++first) { + *pos = *first; + } + std::destroy(pos, end()); + pos = std::uninitialized_copy(first, last, pos); + m_size = pos - begin(); + assert(m_size <= N); + } + + reference at(size_type index) + { + if (index >= m_size) { throw std::out_of_range{"index out of range"}; } + return data()[index]; + } + + const_reference at(size_type index) const + { + if (index >= m_size) { throw std::out_of_range{"index out of range"}; } + return data()[index]; + } + + reference operator[](size_type index) noexcept + { + assert(index < m_size); + return data()[index]; + } + + const_reference operator[](size_type index) const noexcept + { + assert(index < m_size); + return data()[index]; + } + + reference front() noexcept { return operator[](0); } + const_reference front() const noexcept { return operator[](0); } + + reference back() noexcept { return operator[](m_size - 1); } + const_reference back() const noexcept { return operator[](m_size - 1); } + + pointer data() noexcept { return *std::launder(reinterpret_cast(m_data)); } + const_pointer data() const noexcept { return *std::launder(reinterpret_cast(m_data)); } + + iterator begin() noexcept { return data(); } + const_iterator begin() const noexcept { return data(); } + const_iterator cbegin() const noexcept { return data(); } + + iterator end() noexcept { return data() + m_size; } + const_iterator end() const noexcept { return data() + m_size; } + const_iterator cend() const noexcept { return data() + m_size; } + + reverse_iterator rbegin() noexcept { return std::reverse_iterator{end()}; } + const_reverse_iterator rbegin() const noexcept { return std::reverse_iterator{end()}; } + const_reverse_iterator crbegin() const noexcept { return std::reverse_iterator{cend()}; } + + reverse_iterator rend() noexcept { return std::reverse_iterator{begin()}; } + const_reverse_iterator rend() const noexcept { return std::reverse_iterator{begin()}; } + const_reverse_iterator crend() const noexcept { return std::reverse_iterator{cbegin()}; } + + bool empty() const noexcept { return m_size == 0; } + bool full() const noexcept { return m_size == N; } + size_type size() const noexcept { return m_size; } + size_type max_size() const noexcept { return N; } + size_type capacity() const noexcept { return N; } + + void clear() noexcept + { + std::destroy(begin(), end()); + m_size = 0; + } + + iterator insert(const_iterator pos, const T& value) { return emplace(pos, value); } + iterator insert(const_iterator pos, T&& value) { return emplace(pos, std::move(value)); } + + iterator insert(const_iterator pos, size_type count, const T& value) + { + assert(m_size + count <= N); + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + const auto newEnd = std::uninitialized_fill_n(end(), count, value); + std::rotate(mutPos, end(), newEnd); + m_size += count; + return mutPos; + } + + template, int> = 0> + iterator insert(const_iterator pos, It first, It last) + { + // Can't check the size first as the iterator may not be multipass + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + const auto newEnd = std::uninitialized_copy(first, last, end()); + std::rotate(mutPos, end(), newEnd); + m_size = newEnd - begin(); + assert(m_size <= N); + return mutPos; + } + + iterator insert(const_iterator pos, std::initializer_list il) { return insert(pos, il.begin(), il.end()); } + + template + iterator emplace(const_iterator pos, Args&&... args) + { + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + emplace_back(std::forward(args)...); + std::rotate(mutPos, end() - 1, end()); + return mutPos; + } + + iterator erase(const_iterator pos) { return erase(pos, pos + 1); } + iterator erase(const_iterator first, const_iterator last) + { + assert(cbegin() <= first && first <= last && last <= cend()); + const auto mutFirst = begin() + (first - cbegin()); + const auto mutLast = begin() + (last - cbegin()); + const auto newEnd = std::move(mutLast, end(), mutFirst); + std::destroy(newEnd, end()); + m_size = newEnd - begin(); + return mutFirst; + } + + void push_back(const T& value) { emplace_back(value); } + void push_back(T&& value) { emplace_back(std::move(value)); } + + template + reference emplace_back(Args&&... args) + { + assert(!full()); + // TODO C++20: Use std::construct_at + const auto result = new(static_cast(end())) T(std::forward(args)...); + ++m_size; + return *result; + } + + void pop_back() + { + assert(!empty()); + --m_size; + std::destroy_at(end()); + } + + void resize(size_type size) + { + if (size > N) { throw std::length_error{"size exceeds maximum size"}; } + if (size < m_size) { + std::destroy(begin() + size, end()); + } else { + std::uninitialized_value_construct(end(), begin() + size); + } + m_size = size; + } + + void resize(size_type size, const value_type& value) + { + if (size > N) { throw std::length_error{"size exceeds maximum size"}; } + if (size < m_size) { + std::destroy(begin() + size, end()); + } else { + std::uninitialized_fill(end(), begin() + size, value); + } + m_size = size; + } + + void swap(ArrayVector& other) + noexcept(std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v) + { + using std::swap; + swap(*this, other); + } + + friend void swap(ArrayVector& a, ArrayVector& b) + noexcept(std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v) + { + const auto toSwap = std::min(a.size(), b.size()); + const auto aSwapEnd = a.begin() + toSwap; + const auto bSwapEnd = b.begin() + toSwap; + std::swap_ranges(a.begin(), aSwapEnd, b.begin()); + std::uninitialized_move(aSwapEnd, a.end(), bSwapEnd); + std::uninitialized_move(bSwapEnd, b.end(), aSwapEnd); + std::destroy(aSwapEnd, a.end()); + std::destroy(bSwapEnd, b.end()); + std::swap(a.m_size, b.m_size); + } + + // TODO C++20: Replace with operator<=> + friend bool operator<(const ArrayVector& l, const ArrayVector& r) + { + return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); + } + friend bool operator<=(const ArrayVector& l, const ArrayVector& r) { return !(r < l); } + friend bool operator>(const ArrayVector& l, const ArrayVector& r) { return r < l; } + friend bool operator>=(const ArrayVector& l, const ArrayVector& r) { return !(l < r); } + + friend bool operator==(const ArrayVector& l, const ArrayVector& r) + { + return std::equal(l.begin(), l.end(), r.begin(), r.end()); + } + // TODO C++20: Remove + friend bool operator!=(const ArrayVector& l, const ArrayVector& r) { return !(l == r); } + +private: + alignas(T) std::byte m_data[std::max(N * sizeof(T), std::size_t{1})]; // Intentionally a raw array + size_type m_size = 0; +}; + +} // namespace lmms + +#endif // LMMS_ARRAY_VECTOR_H diff --git a/include/AudioEngine.h b/include/AudioEngine.h index 030c5bce39e..d3d0d025ffc 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -197,6 +197,7 @@ class LMMS_EXPORT AudioEngine : public QObject // audio-device-stuff + bool renderOnly() const { return m_renderOnly; } // Returns the current audio device's name. This is not necessarily // the user's preferred audio device, in case you were thinking that. inline const QString & audioDevName() const @@ -275,6 +276,11 @@ class LMMS_EXPORT AudioEngine : public QObject return m_profiler.cpuLoad(); } + int detailLoad(const AudioEngineProfiler::DetailType type) const + { + return m_profiler.detailLoad(type); + } + const qualitySettings & currentQualitySettings() const { return m_qualitySettings; @@ -401,6 +407,10 @@ class LMMS_EXPORT AudioEngine : public QObject AudioDevice * tryAudioDevices(); MidiClient * tryMidiClients(); + void renderStageNoteSetup(); + void renderStageInstruments(); + void renderStageEffects(); + void renderStageMix(); const surroundSampleFrame * renderNextBuffer(); diff --git a/include/AudioEngineProfiler.h b/include/AudioEngineProfiler.h index 7b5191e76b4..b0d62a1dc9e 100644 --- a/include/AudioEngineProfiler.h +++ b/include/AudioEngineProfiler.h @@ -25,6 +25,8 @@ #ifndef LMMS_AUDIO_ENGINE_PROFILER_H #define LMMS_AUDIO_ENGINE_PROFILER_H +#include +#include #include #include "lmms_basics.h" @@ -53,11 +55,55 @@ class AudioEngineProfiler void setOutputFile( const QString& outputFile ); + enum class DetailType { + NoteSetup, + Instruments, + Effects, + Mixing, + Count + }; + + constexpr static auto DetailCount = static_cast(DetailType::Count); + + int detailLoad(const DetailType type) const + { + return m_detailLoad[static_cast(type)].load(std::memory_order_relaxed); + } + + class Probe + { + public: + Probe(AudioEngineProfiler& profiler, AudioEngineProfiler::DetailType type) + : m_profiler(profiler) + , m_type(type) + { + profiler.startDetail(type); + } + ~Probe() { m_profiler.finishDetail(m_type); } + Probe& operator=(const Probe&) = delete; + Probe(const Probe&) = delete; + Probe(Probe&&) = delete; + + private: + AudioEngineProfiler &m_profiler; + const AudioEngineProfiler::DetailType m_type; + }; private: + void startDetail(const DetailType type) { m_detailTimer[static_cast(type)].reset(); } + void finishDetail(const DetailType type) + { + m_detailTime[static_cast(type)] = m_detailTimer[static_cast(type)].elapsed(); + } + MicroTimer m_periodTimer; - int m_cpuLoad; + std::atomic m_cpuLoad; QFile m_outputFile; + + // Use arrays to avoid dynamic allocations in realtime code + std::array m_detailTimer; + std::array m_detailTime{0}; + std::array, DetailCount> m_detailLoad{0}; }; } // namespace lmms diff --git a/include/CPULoadWidget.h b/include/CPULoadWidget.h index 904445c67f0..dfa5bac73da 100644 --- a/include/CPULoadWidget.h +++ b/include/CPULoadWidget.h @@ -26,6 +26,7 @@ #ifndef LMMS_GUI_CPU_LOAD_WIDGET_H #define LMMS_GUI_CPU_LOAD_WIDGET_H +#include #include #include #include @@ -40,6 +41,7 @@ namespace lmms::gui class CPULoadWidget : public QWidget { Q_OBJECT + Q_PROPERTY(int stepSize MEMBER m_stepSize) public: CPULoadWidget( QWidget * _parent ); ~CPULoadWidget() override = default; @@ -54,6 +56,8 @@ protected slots: private: + int stepSize() const { return std::max(1, m_stepSize); } + int m_currentLoad; QPixmap m_temp; @@ -64,6 +68,8 @@ protected slots: QTimer m_updateTimer; + int m_stepSize; + } ; diff --git a/include/DataFile.h b/include/DataFile.h index 137f0156fe2..dc82315adb7 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -126,6 +126,8 @@ class LMMS_EXPORT DataFile : public QDomDocument void upgrade_defaultTripleOscillatorHQ(); void upgrade_mixerRename(); void upgrade_bbTcoRename(); + void upgrade_sampleAndHold(); + void upgrade_midiCCIndexing(); // List of all upgrade methods static const std::vector UPGRADE_METHODS; diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 3334a73f659..eafb827dac5 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -75,8 +75,7 @@ class FileBrowser : public SideBarWidget private slots: void reloadTree(); void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); - // call with item=NULL to filter the entire tree - bool filterItems( const QString & filter, QTreeWidgetItem * item=nullptr ); + bool filterAndExpandItems(const QString & filter, QTreeWidgetItem * item = nullptr); void giveFocusToFilter(); private: @@ -84,6 +83,9 @@ private slots: void addItems( const QString & path ); + void saveDirectoriesStates(); + void restoreDirectoriesStates(); + FileBrowserTreeWidget * m_fileBrowserTreeWidget; QLineEdit * m_filterEdit; @@ -99,6 +101,8 @@ private slots: QCheckBox* m_showFactoryContent = nullptr; QString m_userDir; QString m_factoryDir; + QList m_savedExpandedDirs; + QString m_previousFilterValue; } ; @@ -115,7 +119,6 @@ class FileBrowserTreeWidget : public QTreeWidget //! that are expanded in the tree. QList expandedDirs( QTreeWidgetItem * item = nullptr ) const; - protected: void contextMenuEvent( QContextMenuEvent * e ) override; void mousePressEvent( QMouseEvent * me ) override; diff --git a/include/InstrumentPlayHandle.h b/include/InstrumentPlayHandle.h index bbf53d16c21..dc744b4ffdb 100644 --- a/include/InstrumentPlayHandle.h +++ b/include/InstrumentPlayHandle.h @@ -26,62 +26,33 @@ #define LMMS_INSTRUMENT_PLAY_HANDLE_H #include "PlayHandle.h" -#include "Instrument.h" -#include "NotePlayHandle.h" #include "lmms_export.h" namespace lmms { +class Instrument; +class InstrumentTrack; + class LMMS_EXPORT InstrumentPlayHandle : public PlayHandle { public: - InstrumentPlayHandle( Instrument * instrument, InstrumentTrack* instrumentTrack ); + InstrumentPlayHandle(Instrument * instrument, InstrumentTrack* instrumentTrack); ~InstrumentPlayHandle() override = default; - - void play( sampleFrame * _working_buffer ) override - { - // ensure that all our nph's have been processed first - ConstNotePlayHandleList nphv = NotePlayHandle::nphsOfInstrumentTrack( m_instrument->instrumentTrack(), true ); - - bool nphsLeft; - do - { - nphsLeft = false; - for( const NotePlayHandle * constNotePlayHandle : nphv ) - { - NotePlayHandle * notePlayHandle = const_cast( constNotePlayHandle ); - if( notePlayHandle->state() != ThreadableJob::ProcessingState::Done && - !notePlayHandle->isFinished()) - { - nphsLeft = true; - notePlayHandle->process(); - } - } - } - while( nphsLeft ); - - m_instrument->play( _working_buffer ); - } + void play(sampleFrame * working_buffer) override; bool isFinished() const override { return false; } - bool isFromTrack( const Track* _track ) const override - { - return m_instrument->isFromTrack( _track ); - } - + bool isFromTrack(const Track* track) const override; private: Instrument* m_instrument; - -} ; - +}; } // namespace lmms diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index f21723363e3..5efafe0c72b 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -51,7 +51,7 @@ namespace gui class InstrumentTrackView; class InstrumentTrackWindow; -class InstrumentMiscView; +class InstrumentTuningView; class MidiCCRackView; } // namespace gui @@ -315,7 +315,7 @@ protected slots: friend class gui::InstrumentTrackView; friend class gui::InstrumentTrackWindow; friend class NotePlayHandle; - friend class gui::InstrumentMiscView; + friend class gui::InstrumentTuningView; friend class gui::MidiCCRackView; } ; diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index 363f5b3abc0..d7d5fb83a39 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -25,6 +25,7 @@ #ifndef LMMS_GUI_INSTRUMENT_TRACK_VIEW_H #define LMMS_GUI_INSTRUMENT_TRACK_VIEW_H +#include "MixerLineLcdSpinBox.h" #include "TrackView.h" #include "InstrumentTrack.h" @@ -72,6 +73,7 @@ class InstrumentTrackView : public TrackView protected: + void modelChanged() override; void dragEnterEvent( QDragEnterEvent * _dee ) override; void dropEvent( QDropEvent * _de ) override; @@ -97,6 +99,7 @@ private slots: // widgets in track-settings-widget TrackLabelButton * m_tlb; + MixerLineLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/InstrumentTrackWindow.h b/include/InstrumentTrackWindow.h index d41bbdac894..971c63899c3 100644 --- a/include/InstrumentTrackWindow.h +++ b/include/InstrumentTrackWindow.h @@ -47,7 +47,7 @@ class MixerLineLcdSpinBox; class InstrumentFunctionArpeggioView; class InstrumentFunctionNoteStackingView; class InstrumentMidiIOView; -class InstrumentMiscView; +class InstrumentTuningView; class InstrumentSoundShapingView; class InstrumentTrackShapingView; class InstrumentTrackView; @@ -154,7 +154,7 @@ protected slots: InstrumentFunctionArpeggioView* m_arpeggioView; InstrumentMidiIOView * m_midiView; EffectRackView * m_effectView; - InstrumentMiscView *m_miscView; + InstrumentTuningView *m_tuningView; // test-piano at the bottom of every instrument-settings-window diff --git a/include/InstrumentMiscView.h b/include/InstrumentTuningView.h similarity index 72% rename from include/InstrumentMiscView.h rename to include/InstrumentTuningView.h index 28f3c6a8e63..4ee18dc8466 100644 --- a/include/InstrumentMiscView.h +++ b/include/InstrumentTuningView.h @@ -1,9 +1,9 @@ /* - * InstrumentMiscView.h - widget in instrument-track-window for setting up - * miscellaneous options not covered by other tabs + * InstrumentTuningView.h - widget in instrument-track-window for setting up + * tuning and transposition options * * Copyright (c) 2005-2014 Tobias Doerffel - * Copyright (c) 2020 Martin Pavelek + * Copyright (c) 2020-2022 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -24,11 +24,13 @@ * */ -#ifndef LMMS_GUI_INSTRUMENT_MISC_VIEW_H -#define LMMS_GUI_INSTRUMENT_MISC_VIEW_H +#ifndef LMMS_GUI_INSTRUMENT_TUNING_VIEW_H +#define LMMS_GUI_INSTRUMENT_TUNING_VIEW_H #include +class QLabel; + namespace lmms { @@ -42,15 +44,17 @@ class GroupBox; class LedCheckBox; -class InstrumentMiscView : public QWidget +class InstrumentTuningView : public QWidget { Q_OBJECT public: - InstrumentMiscView(InstrumentTrack *it, QWidget *parent); + InstrumentTuningView(InstrumentTrack *it, QWidget *parent); GroupBox *pitchGroupBox() {return m_pitchGroupBox;} GroupBox *microtunerGroupBox() {return m_microtunerGroupBox;} + QLabel *microtunerNotSupportedLabel() {return m_microtunerNotSupportedLabel;} + ComboBox *scaleCombo() {return m_scaleCombo;} ComboBox *keymapCombo() {return m_keymapCombo;} @@ -60,6 +64,8 @@ class InstrumentMiscView : public QWidget GroupBox *m_pitchGroupBox; GroupBox *m_microtunerGroupBox; + QLabel *m_microtunerNotSupportedLabel; + ComboBox *m_scaleCombo; ComboBox *m_keymapCombo; @@ -71,4 +77,4 @@ class InstrumentMiscView : public QWidget } // namespace lmms -#endif // LMMS_GUI_INSTRUMENT_MISC_VIEW_H +#endif // LMMS_GUI_INSTRUMENT_TUNING_VIEW_H diff --git a/include/Knob.h b/include/Knob.h index 85a51e363e8..d5739bb1c3d 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -144,6 +144,9 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView void wheelEvent( QWheelEvent * _me ) override; void changeEvent(QEvent * ev) override; + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; + virtual float getValue( const QPoint & _p ); private slots: @@ -160,6 +163,7 @@ private slots: float _innerRadius = 1) const; void drawKnob( QPainter * _p ); + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); void setPosition( const QPoint & _p ); bool updateAngle(); diff --git a/include/LfoController.h b/include/LfoController.h index 1c63ba69883..109edbd3fd8 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -86,6 +86,7 @@ public slots: sample_t (*m_sampleFunction)( const float ); private: + float m_heldSample; SampleBuffer * m_userDefSampleBuffer; protected slots: diff --git a/include/LmmsSemaphore.h b/include/LmmsSemaphore.h new file mode 100644 index 00000000000..4170eef6c65 --- /dev/null +++ b/include/LmmsSemaphore.h @@ -0,0 +1,93 @@ +/* + * Semaphore.h - Semaphore declaration + * + * Copyright (c) 2022-2022 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. + * + */ + +/* + * This code has been copied and adapted from https://github.com/drobilla/jalv + * File src/zix/sem.h + */ + +#ifndef LMMS_SEMAPHORE_H +#define LMMS_SEMAPHORE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_BUILD_APPLE +# include +#elif defined(LMMS_BUILD_WIN32) +# include +#else +# include +#endif + +#include + +namespace lmms { + +/** + A counting semaphore. + + This is an integer that is always positive, and has two main operations: + increment (post) and decrement (wait). If a decrement can not be performed + (i.e. the value is 0) the caller will be blocked until another thread posts + and the operation can succeed. + + Semaphores can be created with any starting value, but typically this will + be 0 so the semaphore can be used as a simple signal where each post + corresponds to one wait. + + Semaphores are very efficient (much moreso than a mutex/cond pair). In + particular, at least on Linux, post is async-signal-safe, which means it + does not block and will not be interrupted. If you need to signal from + a realtime thread, this is the most appropriate primitive to use. + + @note Likely outdated with C++20's std::counting_semaphore + (though we have to check that this will be RT conforming on all platforms) +*/ +class Semaphore +{ +public: + Semaphore(unsigned initial); + Semaphore(const Semaphore&) = delete; + Semaphore& operator=(const Semaphore&) = delete; + Semaphore(Semaphore&&) = delete; + Semaphore& operator=(Semaphore&&) = delete; + ~Semaphore(); + + void post(); + void wait(); + bool tryWait(); + +private: +#ifdef LMMS_BUILD_APPLE + semaphore_t m_sem; +#elif defined(LMMS_BUILD_WIN32) + HANDLE m_sem; +#else + sem_t m_sem; +#endif +}; + +} // namespace lmms + +#endif // LMMS_SEMAPHORE_H diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 99c48cc904a..2d65badfe58 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -51,13 +51,14 @@ class LocklessRingBuffer 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();} - std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) + std::size_t write(const T *src, std::size_t cnt, bool notify = false) { std::size_t written = LocklessRingBuffer::m_buffer.write(src, cnt); // Let all waiting readers know new data are available. if (notify) {LocklessRingBuffer::m_notifier.wakeAll();} return written; } + void mlock() { m_buffer.mlock(); } protected: ringbuffer_t m_buffer; diff --git a/include/Lv2Basics.h b/include/Lv2Basics.h index 9a958d9736a..53489e30d10 100644 --- a/include/Lv2Basics.h +++ b/include/Lv2Basics.h @@ -47,8 +47,14 @@ struct LilvNodesDeleter void operator()(LilvNodes* n) { lilv_nodes_free(n); } }; +struct LilvScalePointsDeleter +{ + void operator()(LilvScalePoints* s) { lilv_scale_points_free(s); } +}; + using AutoLilvNode = std::unique_ptr; using AutoLilvNodes = std::unique_ptr; +using AutoLilvScalePoints = std::unique_ptr; /** Return QString from a plugin's node, everything will be freed automatically diff --git a/include/Lv2Features.h b/include/Lv2Features.h index b5bc284c87d..69a456bbde3 100644 --- a/include/Lv2Features.h +++ b/include/Lv2Features.h @@ -30,6 +30,7 @@ #ifdef LMMS_HAVE_LV2 #include +#include #include #include "Lv2Manager.h" @@ -78,7 +79,7 @@ class Lv2Features //! pointers to m_features, required for lilv_plugin_instantiate std::vector m_featurePointers; //! features + data, ordered by URI - std::map m_featureByUri; + std::map m_featureByUri; }; diff --git a/include/Lv2Manager.h b/include/Lv2Manager.h index 909dba5607e..58126a0a448 100644 --- a/include/Lv2Manager.h +++ b/include/Lv2Manager.h @@ -31,6 +31,7 @@ #include #include +#include #include #include "Lv2Basics.h" @@ -120,15 +121,9 @@ class Lv2Manager Iterator begin() { return m_lv2InfoMap.begin(); } Iterator end() { return m_lv2InfoMap.end(); } - //! strcmp based key comparator for std::set and std::map - struct CmpStr - { - bool operator()(char const *a, char const *b) const; - }; - UridMap& uridMap() { return m_uridMap; } const Lv2UridCache& uridCache() const { return m_uridCache; } - const std::set& supportedFeatureURIs() const + const std::set& supportedFeatureURIs() const { return m_supportedFeatureURIs; } @@ -136,17 +131,21 @@ class Lv2Manager AutoLilvNodes findNodes(const LilvNode *subject, const LilvNode *predicate, const LilvNode *object); - static const std::set& getPluginBlacklist() + static const std::set& getPluginBlacklist() { return pluginBlacklist; } + static const std::set& getPluginBlacklistBuffersizeLessThan32() + { + return pluginBlacklistBuffersizeLessThan32; + } private: // general data bool m_debug; //!< if set, debug output will be printed LilvWorld* m_world; Lv2InfoMap m_lv2InfoMap; - std::set m_supportedFeatureURIs; + std::set m_supportedFeatureURIs; // feature data that are common for all Lv2Proc UridMap m_uridMap; @@ -155,7 +154,8 @@ class Lv2Manager Lv2UridCache m_uridCache; // static - static const std::set pluginBlacklist; + static const std::set + pluginBlacklist, pluginBlacklistBuffersizeLessThan32; // functions bool isSubclassOf(const LilvPluginClass *clvss, const char *uriStr); diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 62070def7f6..76fa5eec25b 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -1,7 +1,7 @@ /* * Lv2Proc.h - Lv2 processor class * - * Copyright (c) 2019-2020 Johannes Lorenz + * Copyright (c) 2019-2022 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -31,11 +31,14 @@ #include #include +#include +#include "LinkedModelGroups.h" +#include "LmmsSemaphore.h" #include "Lv2Basics.h" #include "Lv2Features.h" #include "Lv2Options.h" -#include "LinkedModelGroups.h" +#include "Lv2Worker.h" #include "Plugin.h" #include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" #include "TimePos.h" @@ -174,8 +177,14 @@ class Lv2Proc : public LinkedModelGroup const LilvPlugin* m_plugin; LilvInstance* m_instance; Lv2Features m_features; + + // options Lv2Options m_options; + // worker + std::optional m_worker; + Semaphore m_workLock; // this must be shared by different workers + // full list of ports std::vector> m_ports; // quick reference to specific, unique ports diff --git a/include/Lv2ViewBase.h b/include/Lv2ViewBase.h index f7d0e9bcb4f..3c8f1bc3faf 100644 --- a/include/Lv2ViewBase.h +++ b/include/Lv2ViewBase.h @@ -56,7 +56,7 @@ class Lv2ViewProc : public LinkedModelGroupView { public: //! @param colNum numbers of columns for the controls - Lv2ViewProc(QWidget *parent, Lv2Proc *ctrlBase, int colNum); + Lv2ViewProc(QWidget *parent, Lv2Proc *proc, int colNum); ~Lv2ViewProc() override = default; private: diff --git a/include/Lv2Worker.h b/include/Lv2Worker.h new file mode 100644 index 00000000000..7931f8e7cde --- /dev/null +++ b/include/Lv2Worker.h @@ -0,0 +1,93 @@ +/* + * Lv2Worker.h - Lv2Worker class + * + * Copyright (c) 2022-2022 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 LV2WORKER_H +#define LV2WORKER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include +#include + +#include "LocklessRingBuffer.h" +#include "LmmsSemaphore.h" + +namespace lmms +{ + +/** + Worker container +*/ +class Lv2Worker +{ +public: + // CTOR/DTOR/feature access + Lv2Worker(const LV2_Worker_Interface* iface, Semaphore* common_work_lock, bool threaded); + ~Lv2Worker(); + void setHandle(LV2_Handle handle) { m_handle = handle; } + LV2_Worker_Schedule* feature() { return &m_scheduleFeature; } + + // public API + void emitResponses(); + void notifyPluginThatRunFinished() + { + if(m_iface->end_run) { m_iface->end_run(m_scheduleFeature.handle); } + } + + // to be called only by static functions + LV2_Worker_Status scheduleWork(uint32_t size, const void* data); + LV2_Worker_Status respond(uint32_t size, const void* data); + +private: + // functions + void workerFunc(); + std::size_t bufferSize() const; //!< size of internal buffers + + // parameters + const LV2_Worker_Interface* m_iface; + bool m_threaded; + LV2_Handle m_handle; + LV2_Worker_Schedule m_scheduleFeature; + + // threading/synchronization + std::thread m_thread; + std::vector m_response; //!< buffer where single requests from m_requests are unpacked + LocklessRingBuffer m_requests, m_responses; //!< ringbuffer to queue multiple requests + LocklessRingBufferReader m_requestsReader, m_responsesReader; + std::atomic m_exit = false; //!< Whether the worker function should keep looping + Semaphore m_sem; + Semaphore* m_workLock; +}; + + +} // namespace lmms + +#endif // LMMS_HAVE_LV2 + +#endif // LV2WORKER_H + diff --git a/include/MidiClip.h b/include/MidiClip.h index 43b322f8075..c2287bd009e 100644 --- a/include/MidiClip.h +++ b/include/MidiClip.h @@ -79,7 +79,7 @@ class LMMS_EXPORT MidiClip : public Clip void setStep( int step, bool enabled ); // Split the list of notes on the given position - void splitNotes(NoteVector notes, TimePos pos); + void splitNotes(const NoteVector& notes, TimePos pos); // clip-type stuff inline Type type() const diff --git a/include/MidiController.h b/include/MidiController.h index c4ef49590a6..9f49627acc6 100644 --- a/include/MidiController.h +++ b/include/MidiController.h @@ -48,6 +48,7 @@ class MidiController : public Controller, public MidiEventProcessor { Q_OBJECT public: + static constexpr int NONE = -1; MidiController( Model * _parent ); ~MidiController() override = default; diff --git a/include/Note.h b/include/Note.h index 5e3a1b8a2bd..2df196af20a 100644 --- a/include/Note.h +++ b/include/Note.h @@ -91,7 +91,7 @@ const int DefaultMiddleKey = Octave::Octave_4 + Key::C; const int DefaultBaseKey = Octave::Octave_4 + Key::A; const float DefaultBaseFreq = 440.f; -const float MaxDetuning = 4 * 12.0f; +const float MaxDetuning = 5 * 12.0f; diff --git a/include/NotePlayHandle.h b/include/NotePlayHandle.h index 46b14c4cd44..7105d6672cd 100644 --- a/include/NotePlayHandle.h +++ b/include/NotePlayHandle.h @@ -108,6 +108,9 @@ class LMMS_EXPORT NotePlayHandle : public PlayHandle, public Note return m_unpitchedFrequency; } + //! Get the current per-note detuning for this note + float currentDetuning() const { return m_baseDetuning->value(); } + /*! Renders one chunk using the attached instrument into the buffer */ void play( sampleFrame* buffer ) override; @@ -245,7 +248,7 @@ class LMMS_EXPORT NotePlayHandle : public PlayHandle, public Note } /*! Process note detuning automation */ - void processTimePos( const TimePos& time ); + void processTimePos(const TimePos& time, float pitchValue, bool isRecording); /*! Updates total length (m_frames) depending on a new tempo */ void resize( const bpm_t newTempo ); diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 9f3bbcd7d20..38788180f8f 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -308,9 +308,9 @@ protected slots: TimePos newNoteLen() const; void shiftPos(int amount); - void shiftPos(NoteVector notes, int amount); + void shiftPos(const NoteVector& notes, int amount); void shiftSemiTone(int amount); - void shiftSemiTone(NoteVector notes, int amount); + void shiftSemiTone(const NoteVector& notes, int amount); bool isSelection() const; int selectionCount() const; void testPlayNote( Note * n ); diff --git a/include/SampleClip.h b/include/SampleClip.h index c9e247328ca..5246787bdc6 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -77,7 +77,7 @@ class SampleClip : public Clip public slots: void setSampleBuffer( lmms::SampleBuffer* sb ); - void setSampleFile( const QString & _sf ); + void setSampleFile( const QString & sf ); void updateLength(); void toggleRecord(); void playbackPositionChanged(); diff --git a/include/SampleTrackView.h b/include/SampleTrackView.h index b586df15edc..3ccb97aeaa9 100644 --- a/include/SampleTrackView.h +++ b/include/SampleTrackView.h @@ -26,6 +26,7 @@ #define LMMS_GUI_SAMPLE_TRACK_VIEW_H +#include "MixerLineLcdSpinBox.h" #include "TrackView.h" namespace lmms @@ -90,6 +91,7 @@ private slots: private: SampleTrackWindow * m_window; + MixerLineLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/SetupDialog.h b/include/SetupDialog.h index de4cdd9ddbd..fa41325db79 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -102,6 +102,7 @@ private slots: // Audio settings widget. void audioInterfaceChanged(const QString & driver); void toggleHQAudioDev(bool enabled); + void updateBufferSizeWarning(int value); void setBufferSize(int value); void resetBufferSize(); @@ -179,6 +180,7 @@ private slots: int m_bufferSize; QSlider * m_bufferSizeSlider; QLabel * m_bufferSizeLbl; + QLabel * m_bufferSizeWarnLbl; // MIDI settings widgets. QComboBox * m_midiInterfaces; diff --git a/include/SimpleTextFloat.h b/include/SimpleTextFloat.h index f720d0b3ef3..bde6c84faab 100644 --- a/include/SimpleTextFloat.h +++ b/include/SimpleTextFloat.h @@ -31,6 +31,7 @@ #include "lmms_export.h" class QLabel; +class QTimer; namespace lmms::gui { @@ -44,6 +45,8 @@ class LMMS_EXPORT SimpleTextFloat : public QWidget void setText(const QString & text); + void showWithDelay(int msecBeforeDisplay, int msecDisplayTime); + void setVisibilityTimeOut(int msecs); void moveGlobal(QWidget * w, const QPoint & offset) @@ -51,11 +54,14 @@ class LMMS_EXPORT SimpleTextFloat : public QWidget move(w->mapToGlobal(QPoint(0, 0)) + offset); } + void hide(); + private: QLabel * m_textLabel; + QTimer * m_showTimer; + QTimer * m_hideTimer; }; - } // namespace lmms::gui #endif diff --git a/include/TrackView.h b/include/TrackView.h index 763705599c5..f697d9ea86a 100644 --- a/include/TrackView.h +++ b/include/TrackView.h @@ -48,11 +48,11 @@ class FadeButton; class TrackContainerView; -const int DEFAULT_SETTINGS_WIDGET_WIDTH = 224; +const int DEFAULT_SETTINGS_WIDGET_WIDTH = 256; const int TRACK_OP_WIDTH = 78; // This shaves 150-ish pixels off track buttons, // ruled from config: ui.compacttrackbuttons -const int DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT = 96; +const int DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT = 128; const int TRACK_OP_WIDTH_COMPACT = 62; diff --git a/include/lmms_math.h b/include/lmms_math.h index b62da81c249..ea0a75581e8 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -325,6 +325,32 @@ static inline T absMin( T a, T b ) return std::abs(a) < std::abs(b) ? a : b; } +// @brief Calculate number of digits which LcdSpinBox would show for a given number +// @note Once we upgrade to C++20, we could probably use std::formatted_size +static inline int numDigitsAsInt(float f) +{ + // use rounding: + // LcdSpinBox sometimes uses roundf(), sometimes cast rounding + // we use rounding to be on the "safe side" + const float rounded = roundf(f); + int asInt = static_cast(rounded); + int digits = 1; // always at least 1 + if(asInt < 0) + { + ++digits; + asInt = -asInt; + } + // "asInt" is positive from now + int32_t power = 1; + for(int32_t i = 1; i<10; ++i) + { + power *= 10; + if(static_cast(asInt) >= power) { ++digits; } // 2 digits for >=10, 3 for >=100 + else { break; } + } + return digits; +} + } // namespace lmms diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index a941e773f25..6671022070c 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -171,9 +171,6 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, static_cast( m_loopModel.value() ) ) ) { applyRelease( _working_buffer, _n ); - instrumentTrack()->processAudioBuffer( _working_buffer, - frames + offset, _n ); - emit isPlaying( ((handleState *)_n->m_pluginData)->frameIndex() ); } else diff --git a/plugins/BitInvader/BitInvader.cpp b/plugins/BitInvader/BitInvader.cpp index 98ef1e97cf1..4ea73dc7116 100644 --- a/plugins/BitInvader/BitInvader.cpp +++ b/plugins/BitInvader/BitInvader.cpp @@ -307,8 +307,6 @@ void BitInvader::playNote( NotePlayHandle * _n, } applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index faff94b570b..819736e928b 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -508,7 +508,6 @@ void CarlaInstrument::play(sampleFrame* workingBuffer) if (fHandle == nullptr) { - instrumentTrack()->processAudioBuffer(workingBuffer, bufsize, nullptr); return; } @@ -556,8 +555,6 @@ void CarlaInstrument::play(sampleFrame* workingBuffer) workingBuffer[i][0] = buf1[i]; workingBuffer[i][1] = buf2[i]; } - - instrumentTrack()->processAudioBuffer(workingBuffer, bufsize, nullptr); } bool CarlaInstrument::handleMidiEvent(const MidiEvent& event, const TimePos&, f_cnt_t offset) diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp index 3c5ad615714..0fe13942083 100755 --- a/plugins/Compressor/Compressor.cpp +++ b/plugins/Compressor/Compressor.cpp @@ -177,7 +177,8 @@ void CompressorEffect::calcRange() void CompressorEffect::resizeRMS() { - m_rmsTimeConst = exp(-1.f / (m_compressorControls.m_rmsModel.value() * 0.001f * m_sampleRate)); + const float rmsValue = m_compressorControls.m_rmsModel.value(); + m_rmsTimeConst = (rmsValue > 0) ? exp(-1.f / (rmsValue * 0.001f * m_sampleRate)) : 0; } void CompressorEffect::calcLookaheadLength() @@ -320,8 +321,8 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) float inputValue = feedback ? m_prevOut[i] : s[i]; // Calculate the crest factor of the audio by diving the peak by the RMS - m_crestPeakVal[i] = qMax(inputValue * inputValue, m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); - m_crestRmsVal[i] = m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inputValue * inputValue)); + m_crestPeakVal[i] = qMax(qMax(COMP_NOISE_FLOOR, inputValue * inputValue), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); + m_crestRmsVal[i] = qMax(COMP_NOISE_FLOOR, m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inputValue * inputValue))); m_crestFactorVal[i] = m_crestPeakVal[i] / m_crestRmsVal[i]; m_rmsVal[i] = m_rmsTimeConst * m_rmsVal[i] + ((1 - m_rmsTimeConst) * (inputValue * inputValue)); diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 114980a7d96..1516456a22a 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -497,10 +497,12 @@ void CompressorControlDialog::redrawKnee() float actualRatio = m_controls->m_limiterModel.value() ? 0 : m_controls->m_effect->m_ratioVal; // Calculate endpoints for the two straight lines - float kneePoint1 = m_controls->m_effect->m_thresholdVal - m_controls->m_effect->m_kneeVal; - float kneePoint2X = m_controls->m_effect->m_thresholdVal + m_controls->m_effect->m_kneeVal; - float kneePoint2Y = (m_controls->m_effect->m_thresholdVal + (-m_controls->m_effect->m_thresholdVal * (actualRatio * (m_controls->m_effect->m_kneeVal / -m_controls->m_effect->m_thresholdVal)))); - float ratioPoint = m_controls->m_effect->m_thresholdVal + (-m_controls->m_effect->m_thresholdVal * actualRatio); + const float thresholdVal = m_controls->m_effect->m_thresholdVal; + const float kneeVal = m_controls->m_effect->m_kneeVal; + float kneePoint1 = thresholdVal - kneeVal; + float kneePoint2X = thresholdVal + kneeVal; + float kneePoint2Y = thresholdVal + kneeVal * actualRatio; + float ratioPoint = thresholdVal + (-thresholdVal * actualRatio); // Draw two straight lines m_p.drawLine(0, m_kneeWindowSizeY, dbfsToXPoint(kneePoint1), dbfsToYPoint(kneePoint1)); @@ -510,7 +512,7 @@ void CompressorControlDialog::redrawKnee() } // Draw knee section - if (m_controls->m_effect->m_kneeVal) + if (kneeVal) { m_p.setPen(QPen(m_kneeColor2, 3)); @@ -522,8 +524,8 @@ void CompressorControlDialog::redrawKnee() { newPoint[0] = linearInterpolate(kneePoint1, kneePoint2X, (i + 1) / (float)COMP_KNEE_LINES); - const float temp = newPoint[0] - m_controls->m_effect->m_thresholdVal + m_controls->m_effect->m_kneeVal; - newPoint[1] = (newPoint[0] + (actualRatio - 1) * temp * temp / (4 * m_controls->m_effect->m_kneeVal)); + const float temp = newPoint[0] - thresholdVal + kneeVal; + newPoint[1] = (newPoint[0] + (actualRatio - 1) * temp * temp / (4 * kneeVal)); m_p.drawLine(dbfsToXPoint(prevPoint[0]), dbfsToYPoint(prevPoint[1]), dbfsToXPoint(newPoint[0]), dbfsToYPoint(newPoint[1])); @@ -768,4 +770,4 @@ void CompressorControlDialog::resetCompressorView() } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/plugins/FreeBoy/FreeBoy.cpp b/plugins/FreeBoy/FreeBoy.cpp index 6450253ee45..f2dc95699ed 100644 --- a/plugins/FreeBoy/FreeBoy.cpp +++ b/plugins/FreeBoy/FreeBoy.cpp @@ -419,7 +419,6 @@ void FreeBoyInstrument::playNote(NotePlayHandle* nph, sampleFrame* workingBuffer } framesLeft -= count; } - instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, nph); } diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index c2e155a20c5..0713d310038 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -494,8 +494,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) _working_buffer[i][0] *= m_gain.value(); _working_buffer[i][1] *= m_gain.value(); } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); } diff --git a/plugins/Kicker/Kicker.cpp b/plugins/Kicker/Kicker.cpp index ef1d623c1a1..e6418e2da5b 100644 --- a/plugins/Kicker/Kicker.cpp +++ b/plugins/Kicker/Kicker.cpp @@ -197,8 +197,6 @@ void KickerInstrument::playNote( NotePlayHandle * _n, _working_buffer[f+offset][1] *= fac; } } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/LadspaEffect/calf/CMakeLists.txt b/plugins/LadspaEffect/calf/CMakeLists.txt index 6ec392a81d2..6f7bb8c0110 100644 --- a/plugins/LadspaEffect/calf/CMakeLists.txt +++ b/plugins/LadspaEffect/calf/CMakeLists.txt @@ -36,11 +36,11 @@ TARGET_COMPILE_DEFINITIONS(veal PRIVATE DISABLE_OSC=1) SET(INLINE_FLAGS "") IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - SET(INLINE_FLAGS "-finline-functions-called-once -finline-limit=80") + SET(INLINE_FLAGS -finline-functions-called-once -finline-limit=80) ENDIF() IF(NOT MSVC) - SET_TARGET_PROPERTIES(veal PROPERTIES COMPILE_FLAGS "-fexceptions -O2 -finline-functions ${INLINE_FLAGS}") + target_compile_options(veal PRIVATE -fexceptions -O2 -finline-functions ${INLINE_FLAGS}) endif() if(LMMS_BUILD_WIN32) @@ -53,5 +53,5 @@ if(LMMS_BUILD_WIN32) ) endif() IF(NOT LMMS_BUILD_APPLE AND NOT LMMS_BUILD_OPENBSD) - SET_TARGET_PROPERTIES(veal PROPERTIES LINK_FLAGS "${LINK_FLAGS} -shared -Wl,-no-undefined") + target_link_libraries(veal PRIVATE -shared) ENDIF() diff --git a/plugins/LadspaEffect/caps/Descriptor.h b/plugins/LadspaEffect/caps/Descriptor.h index 12c5d1c8846..c3e1c325e73 100644 --- a/plugins/LadspaEffect/caps/Descriptor.h +++ b/plugins/LadspaEffect/caps/Descriptor.h @@ -53,7 +53,7 @@ class DescriptorStub PortCount = 0; } - ~DescriptorStub() + virtual ~DescriptorStub() { if (PortCount) { @@ -87,6 +87,7 @@ class Descriptor public: Descriptor() { setup(); } + ~Descriptor() override = default; void setup(); void autogen() diff --git a/plugins/LadspaEffect/cmt/CMakeLists.txt b/plugins/LadspaEffect/cmt/CMakeLists.txt index ded7b895898..75dba319d6c 100644 --- a/plugins/LadspaEffect/cmt/CMakeLists.txt +++ b/plugins/LadspaEffect/cmt/CMakeLists.txt @@ -5,7 +5,7 @@ ADD_LIBRARY(cmt MODULE ${SOURCES}) INSTALL(TARGETS cmt LIBRARY DESTINATION "${PLUGIN_DIR}/ladspa") SET_TARGET_PROPERTIES(cmt PROPERTIES PREFIX "") -SET_TARGET_PROPERTIES(cmt PROPERTIES COMPILE_FLAGS "-Wall -O3 -fno-strict-aliasing") +target_compile_options(cmt PRIVATE -Wall -O3 -fno-strict-aliasing) if(LMMS_BUILD_WIN32) add_custom_command( @@ -18,10 +18,10 @@ if(LMMS_BUILD_WIN32) endif() if(NOT LMMS_BUILD_WIN32) - set_target_properties(cmt PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -fPIC") + target_compile_options(cmt PRIVATE -fPIC) endif() IF(NOT LMMS_BUILD_APPLE AND NOT LMMS_BUILD_OPENBSD) - SET_TARGET_PROPERTIES(cmt PROPERTIES LINK_FLAGS "${LINK_FLAGS} -shared -Wl,-no-undefined") + target_link_libraries(cmt PRIVATE -shared) ENDIF() diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index b8fff2c0b8a..ee49442d5d4 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -790,7 +790,6 @@ void Lb302Synth::play( sampleFrame * _working_buffer ) const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); process( _working_buffer, frames ); - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); // release_frame = 0; //removed for issue # 1432 } diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 1e45f4e919e..32f81d23c25 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -197,8 +197,6 @@ void Lv2Instrument::play(sampleFrame *buf) copyModelsToLmms(); copyBuffersToLmms(buf, fpp); - - instrumentTrack()->processAudioBuffer(buf, fpp, nullptr); } diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index f588d6b786d..2201e4ed90b 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -1040,8 +1040,6 @@ void MonstroInstrument::playNote( NotePlayHandle * _n, ms->renderOutput( frames, _working_buffer + offset ); //applyRelease( _working_buffer, _n ); // we have our own release - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } void MonstroInstrument::deleteNotePluginData( NotePlayHandle * _n ) diff --git a/plugins/Nes/Nes.cpp b/plugins/Nes/Nes.cpp index a530ac19b3b..47122a0c602 100644 --- a/plugins/Nes/Nes.cpp +++ b/plugins/Nes/Nes.cpp @@ -561,8 +561,6 @@ void NesInstrument::playNote( NotePlayHandle * n, sampleFrame * workingBuffer ) nes->renderOutput( workingBuffer + offset, frames ); applyRelease( workingBuffer, n ); - - instrumentTrack()->processAudioBuffer( workingBuffer, frames + offset, n ); } diff --git a/plugins/OpulenZ/OpulenZ.cpp b/plugins/OpulenZ/OpulenZ.cpp index 64f60999576..d90d5f343a4 100644 --- a/plugins/OpulenZ/OpulenZ.cpp +++ b/plugins/OpulenZ/OpulenZ.cpp @@ -412,10 +412,6 @@ void OpulenzInstrument::play( sampleFrame * _working_buffer ) } } emulatorMutex.unlock(); - - // Throw the data to the track... - instrumentTrack()->processAudioBuffer( _working_buffer, frameCount, nullptr ); - } diff --git a/plugins/Organic/Organic.cpp b/plugins/Organic/Organic.cpp index f8a2b0d135c..a70da642156 100644 --- a/plugins/Organic/Organic.cpp +++ b/plugins/Organic/Organic.cpp @@ -312,8 +312,6 @@ void OrganicInstrument::playNote( NotePlayHandle * _n, } // -- -- - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index a2b829940a4..24c54d66be5 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -157,8 +157,6 @@ void PatmanInstrument::playNote( NotePlayHandle * _n, play_freq, m_loopedModel.value() ? SampleBuffer::LoopMode::On : SampleBuffer::LoopMode::Off ) ) { applyRelease( _working_buffer, _n ); - instrumentTrack()->processAudioBuffer( _working_buffer, - frames + offset, _n ); } else { @@ -446,7 +444,7 @@ namespace gui PatmanView::PatmanView( Instrument * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ), - m_pi( nullptr ) + m_pi(castModel()) { setAutoFillBackground( true ); QPalette pal; @@ -487,7 +485,15 @@ PatmanView::PatmanView( Instrument * _instrument, QWidget * _parent ) : "tune_off" ) ); m_tuneButton->setToolTip(tr("Tune mode")); - m_displayFilename = tr( "No file selected" ); + + if (m_pi->m_patchFile.isEmpty()) + { + m_displayFilename = tr("No file selected"); + } + else + { + updateFilename(); + } setAcceptDrops( true ); } diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 1f0cb7c59e2..79bd4b97686 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -30,6 +30,7 @@ #include #include +#include "ArrayVector.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "FileDialog.h" @@ -71,17 +72,47 @@ Plugin::Descriptor PLUGIN_EXPORT sf2player_plugin_descriptor = } +/** + * A non-owning reference to a single FluidSynth voice, for tracking whether the + * referenced voice is still the same voice that was passed to the constructor. + */ +class FluidVoice +{ +public: + //! Create a reference to the voice currently pointed at by `voice`. + explicit FluidVoice(fluid_voice_t* voice) : + m_voice{voice}, + m_id{fluid_voice_get_id(voice)} + { } + + //! Get a pointer to the referenced voice. + fluid_voice_t* get() const noexcept { return m_voice; } + + //! Test whether this object still refers to the original voice. + bool isValid() const + { + return fluid_voice_get_id(m_voice) == m_id && fluid_voice_is_playing(m_voice); + } + +private: + fluid_voice_t* m_voice; + unsigned int m_id; +}; struct Sf2PluginData { int midiNote; int lastPanning; float lastVelocity; - fluid_voice_t * fluidVoice; + // The soundfonts I checked used at most two voices per note, so space for + // four should be safe. This may need to be increased if a soundfont with + // more voices per note is found. + ArrayVector fluidVoices; bool isNew; f_cnt_t offset; bool noteOffSent; -} ; + panning_t panning; +}; @@ -681,10 +712,10 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) pluginData->midiNote = midiNote; pluginData->lastPanning = 0; pluginData->lastVelocity = _n->midiVelocity( baseVelocity ); - pluginData->fluidVoice = nullptr; pluginData->isNew = true; pluginData->offset = _n->offset(); pluginData->noteOffSent = false; + pluginData->panning = _n->getPanning(); _n->m_pluginData = pluginData; @@ -703,6 +734,17 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) m_playingNotes.append( _n ); m_playingNotesMutex.unlock(); } + + // Update the pitch of all the voices + if (const auto data = static_cast(_n->m_pluginData)) { + const auto detuning = _n->currentDetuning(); + for (const auto& voice : data->fluidVoices) { + if (voice.isValid()) { + fluid_voice_gen_set(voice.get(), GEN_COARSETUNE, detuning); + fluid_voice_update_param(voice.get(), GEN_COARSETUNE); + } + } + } } @@ -715,34 +757,46 @@ void Sf2Instrument::noteOn( Sf2PluginData * n ) const int poly = fluid_synth_get_polyphony( m_synth ); #ifndef _MSC_VER fluid_voice_t* voices[poly]; - unsigned int id[poly]; #else const auto voices = static_cast(_alloca(poly * sizeof(fluid_voice_t*))); - const auto id = static_cast(_alloca(poly * sizeof(unsigned int))); #endif - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly; ++i ) - { - id[i] = 0; - } - for( int i = 0; i < poly && voices[i]; ++i ) - { - id[i] = fluid_voice_get_id( voices[i] ); - } fluid_synth_noteon( m_synth, m_channel, n->midiNote, n->lastVelocity ); - // get new voice and save it - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly && voices[i]; ++i ) + // Get any new voices and store them in the plugin data + fluid_synth_get_voicelist(m_synth, voices, poly, -1); + for (int i = 0; i < poly && voices[i] && !n->fluidVoices.full(); ++i) { - const unsigned int newID = fluid_voice_get_id( voices[i] ); - if( id[i] != newID || newID == 0 ) - { - n->fluidVoice = voices[i]; - break; + const auto voice = voices[i]; + // FluidSynth stops voices with the same channel and pitch upon note-on, + // so voices with the current channel and pitch are playing this note. + if (fluid_voice_get_channel(voice) == m_channel + && fluid_voice_get_key(voice) == n->midiNote + && fluid_voice_is_on(voice) + ) { + n->fluidVoices.emplace_back(voices[i]); + } + } + +#if FLUIDSYNTH_VERSION_MAJOR >= 2 + // Smallest balance value that results in full attenuation of one channel. + // Corresponds to internal FluidSynth macro `FLUID_CB_AMP_SIZE`. + constexpr static auto maxBalance = 1441.f; + // Convert panning from linear to exponential for FluidSynth + const auto panning = n->panning; + const auto factor = 1.f - std::abs(panning) / static_cast(PanningRight); + const auto balance = std::copysign( + factor <= 0 ? maxBalance : std::min(-200.f * std::log10(factor), maxBalance), + panning + ); + // Set note panning on all the voices + for (const auto& voice : n->fluidVoices) { + if (voice.isValid()) { + fluid_voice_gen_set(voice.get(), GEN_CUSTOM_BALANCE, balance); + fluid_voice_update_param(voice.get(), GEN_CUSTOM_BALANCE); } } +#endif m_synthMutex.unlock(); @@ -794,7 +848,6 @@ void Sf2Instrument::play( sampleFrame * _working_buffer ) if( m_playingNotes.isEmpty() ) { renderFrames( frames, _working_buffer ); - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); return; } @@ -852,13 +905,13 @@ void Sf2Instrument::play( sampleFrame * _working_buffer ) { renderFrames( frames - currentFrame, _working_buffer + currentFrame ); } - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); } void Sf2Instrument::renderFrames( f_cnt_t frames, sampleFrame * buf ) { m_synthMutex.lock(); + fluid_synth_get_gain(m_synth); // This flushes voice updates as a side effect if( m_internalSampleRate < Engine::audioEngine()->processingSampleRate() && m_srcState != nullptr ) { diff --git a/plugins/Sfxr/Sfxr.cpp b/plugins/Sfxr/Sfxr.cpp index fc39ea0fa28..e79b8e2adbe 100644 --- a/plugins/Sfxr/Sfxr.cpp +++ b/plugins/Sfxr/Sfxr.cpp @@ -480,9 +480,6 @@ void SfxrInstrument::playNote( NotePlayHandle * _n, sampleFrame * _working_buffe delete[] pitchedBuffer; applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frameNum + offset, _n ); - } diff --git a/plugins/Sid/CMakeLists.txt b/plugins/Sid/CMakeLists.txt index c9fce7bb77d..c771fc66de6 100644 --- a/plugins/Sid/CMakeLists.txt +++ b/plugins/Sid/CMakeLists.txt @@ -1,51 +1,14 @@ INCLUDE(BuildPlugin) -INCLUDE_DIRECTORIES(resid) +if(NOT LMMS_HAVE_SID) + return() +endif() BUILD_PLUGIN(sid SidInstrument.cpp SidInstrument.h - resid/envelope.h - resid/extfilt.h - resid/filter.h - resid/pot.h - resid/siddefs.h - resid/sid.h - resid/spline.h - resid/voice.h - resid/wave.h - resid/envelope.cc - resid/extfilt.cc - resid/filter.cc - resid/pot.cc - resid/sid.cc - resid/version.cc - resid/voice.cc - resid/wave6581_PS_.cc - resid/wave6581_PST.cc - resid/wave6581_P_T.cc - resid/wave6581__ST.cc - resid/wave8580_PS_.cc - resid/wave8580_PST.cc - resid/wave8580_P_T.cc - resid/wave8580__ST.cc - resid/wave.cc MOCFILES SidInstrument.h EMBEDDED_RESOURCES *.png) -# Parse VERSION -FILE(READ "resid/CMakeLists.txt" lines) -STRING(REGEX MATCH "set\\(MAJOR_VER [A-Za-z0-9_]*\\)" MAJOR_RAW ${lines}) -STRING(REGEX MATCH "set\\(MINOR_VER [A-Za-z0-9_]*\\)" MINOR_RAW ${lines}) -STRING(REGEX MATCH "set\\(PATCH_VER [A-Za-z0-9_]*\\)" PATCH_RAW ${lines}) -SEPARATE_ARGUMENTS(MAJOR_RAW) -SEPARATE_ARGUMENTS(MINOR_RAW) -SEPARATE_ARGUMENTS(PATCH_RAW) -LIST(GET MAJOR_RAW 1 MAJOR_RAW) -LIST(GET MINOR_RAW 1 MINOR_RAW) -LIST(GET PATCH_RAW 1 PATCH_RAW) -STRING(REPLACE ")" "" MAJOR_VER "${MAJOR_RAW}") -STRING(REPLACE ")" "" MINOR_VER "${MINOR_RAW}") -STRING(REPLACE ")" "" PATCH_VER "${PATCH_RAW}") - -TARGET_COMPILE_DEFINITIONS(sid PRIVATE VERSION="${MAJOR_VER}.${MINOR_VER}.${PATCH_VER}") +add_subdirectory(resid) +target_link_libraries(sid resid) diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index f663c3b6977..7f9edf13f18 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -239,7 +239,7 @@ f_cnt_t SidInstrument::desiredReleaseFrames() const -static int sid_fillbuffer(unsigned char* sidreg, SID *sid, int tdelta, short *ptr, int samples) +static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, short *ptr, int samples) { int tdelta2; int result; @@ -302,9 +302,9 @@ void SidInstrument::playNote( NotePlayHandle * _n, if (!_n->m_pluginData) { - SID *sid = new SID(); - sid->set_sampling_parameters( clockrate, SAMPLE_FAST, samplerate ); - sid->set_chip_model( MOS8580 ); + auto sid = new reSID::SID(); + sid->set_sampling_parameters(clockrate, reSID::SAMPLE_FAST, samplerate); + sid->set_chip_model(reSID::MOS8580); sid->enable_filter( true ); sid->reset(); _n->m_pluginData = sid; @@ -312,7 +312,7 @@ void SidInstrument::playNote( NotePlayHandle * _n, const fpp_t frames = _n->framesLeftForCurrentPeriod(); const f_cnt_t offset = _n->noteOffset(); - SID *sid = static_cast( _n->m_pluginData ); + auto sid = static_cast(_n->m_pluginData); int delta_t = clockrate * frames / samplerate + 4; // avoid variable length array for msvc compat auto buf = reinterpret_cast(_working_buffer + offset); @@ -325,20 +325,20 @@ void SidInstrument::playNote( NotePlayHandle * _n, if( (ChipModel)m_chipModel.value() == ChipModel::MOS6581 ) { - sid->set_chip_model( MOS6581 ); + sid->set_chip_model(reSID::MOS6581); } else { - sid->set_chip_model( MOS8580 ); + sid->set_chip_model(reSID::MOS8580); } // voices - reg8 data8 = 0; - reg8 data16 = 0; - reg8 base = 0; + reSID::reg8 data8 = 0; + reSID::reg16 data16 = 0; + size_t base = 0; float freq = 0.0; float note = 0.0; - for( reg8 i = 0 ; i < 3 ; ++i ) + for (size_t i = 0; i < 3; ++i) { base = i*7; // freq ( Fn = Fout / Fclk * 16777216 ) + coarse detuning @@ -429,8 +429,6 @@ void SidInstrument::playNote( NotePlayHandle * _n, _working_buffer[frame+offset][ch] = s; } } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } @@ -438,7 +436,7 @@ void SidInstrument::playNote( NotePlayHandle * _n, void SidInstrument::deleteNotePluginData( NotePlayHandle * _n ) { - delete static_cast( _n->m_pluginData ); + delete static_cast(_n->m_pluginData); } diff --git a/plugins/Sid/resid b/plugins/Sid/resid deleted file mode 160000 index 099506e6f63..00000000000 --- a/plugins/Sid/resid +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 099506e6f63cc9d40fcefacd2a93a846009aa2df diff --git a/plugins/Sid/resid/CMakeLists.txt b/plugins/Sid/resid/CMakeLists.txt new file mode 100644 index 00000000000..bb39e3d166f --- /dev/null +++ b/plugins/Sid/resid/CMakeLists.txt @@ -0,0 +1,68 @@ +# These are the defaults +set(RESID_INLINING 1) +set(RESID_INLINE inline) +set(RESID_BRANCH_HINTS 1) +set(NEW_8580_FILTER 0) + +set(HAVE_BOOL 1) +set(HAVE_LOG1P 1) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GCC|Clang") + set(HAVE_BUILTIN_EXPECT 1) +else() + set(HAVE_BUILTIN_EXPECT 0) +endif() + +configure_file(resid/siddefs.h.in resid/siddefs.h @ONLY) + +add_library(resid_objects OBJECT + resid/sid.cc + resid/voice.cc + resid/wave.cc + resid/envelope.cc + resid/filter.cc + resid/dac.cc + resid/extfilt.cc + resid/pot.cc + resid/version.cc +) + +target_include_directories(resid_objects PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/resid" + "${CMAKE_CURRENT_BINARY_DIR}/resid" +) +target_compile_definitions(resid_objects PUBLIC VERSION="1.0") + +set(RESID_WAVES + wave6581_PST + wave6581_PS_ + wave6581_P_T + wave6581__ST + wave8580_PST + wave8580_PS_ + wave8580_P_T + wave8580__ST +) + +set(RESID_SAMP2SRC_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/resid/samp2src.pl) +foreach(WAVE_DATA IN LISTS RESID_WAVES) + set(WAVE_DATA_IN ${CMAKE_CURRENT_SOURCE_DIR}/resid/${WAVE_DATA}.dat) + set(WAVE_SRC_OUT ${CMAKE_CURRENT_BINARY_DIR}/resid/${WAVE_DATA}.h) + set(WAVE_COMMAND + "${PERL_EXECUTABLE}" + "${RESID_SAMP2SRC_SCRIPT}" + "${WAVE_DATA}" + "${WAVE_DATA_IN}" + "${WAVE_SRC_OUT}" + ) + add_custom_command(OUTPUT ${WAVE_SRC_OUT} COMMAND ${WAVE_COMMAND} VERBATIM) + target_sources(resid_objects PUBLIC ${WAVE_SRC_OUT}) +endforeach() + +# TODO CMake 3.12: Use target_link_libraries() to propagate usage requirements directly to sid plugin +add_library(resid INTERFACE) + +target_sources(resid INTERFACE $) + +get_target_property(resid_includes resid_objects INCLUDE_DIRECTORIES) +target_include_directories(resid INTERFACE ${resid_includes}) diff --git a/plugins/Sid/resid/resid b/plugins/Sid/resid/resid new file mode 160000 index 00000000000..ef72462f5fa --- /dev/null +++ b/plugins/Sid/resid/resid @@ -0,0 +1 @@ +Subproject commit ef72462f5fa0682d099413512b764ae479e77f9b diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index 4fb077de5bc..b746e949120 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -359,8 +359,6 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, _working_buffer[frame][1] = ps->nextSampleRight() * ( m_scalers[p] + add_scale ); } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index f2340d3d609..5b8f6e8ade6 100644 --- a/plugins/TripleOscillator/TripleOscillator.cpp +++ b/plugins/TripleOscillator/TripleOscillator.cpp @@ -380,8 +380,6 @@ void TripleOscillator::playNote( NotePlayHandle * _n, applyFadeIn(_working_buffer, _n); applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index dd8e9cbef3b..a696a4b2ded 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -399,8 +399,6 @@ void VestigeInstrument::play( sampleFrame * _buf ) { if (!m_pluginMutex.tryLock(Engine::getSong()->isExporting() ? -1 : 0)) {return;} - const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - if( m_plugin == nullptr ) { m_pluginMutex.unlock(); @@ -409,8 +407,6 @@ void VestigeInstrument::play( sampleFrame * _buf ) m_plugin->process( nullptr, _buf ); - instrumentTrack()->processAudioBuffer( _buf, frames, nullptr ); - m_pluginMutex.unlock(); } diff --git a/plugins/Vibed/Vibed.cpp b/plugins/Vibed/Vibed.cpp index 3ed51fe79d7..ad6a3942af4 100644 --- a/plugins/Vibed/Vibed.cpp +++ b/plugins/Vibed/Vibed.cpp @@ -251,8 +251,6 @@ void Vibed::playNote(NotePlayHandle* n, sampleFrame* workingBuffer) } } } - - instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, n); } void Vibed::deleteNotePluginData(NotePlayHandle* n) diff --git a/plugins/VstEffect/VstSubPluginFeatures.cpp b/plugins/VstEffect/VstSubPluginFeatures.cpp index f929b5526bb..7eab7a9bf67 100644 --- a/plugins/VstEffect/VstSubPluginFeatures.cpp +++ b/plugins/VstEffect/VstSubPluginFeatures.cpp @@ -82,7 +82,11 @@ void VstSubPluginFeatures::addPluginsFromDir( QStringList* filenames, QString pa } } QStringList dlls = QDir( ConfigManager::inst()->vstDir() + path ). - entryList( QStringList() << "*.dll", + entryList( QStringList() << "*.dll" +#ifdef LMMS_BUILD_LINUX + << "*.so" +#endif + , QDir::Files, QDir::Name ); for( int i = 0; i < dlls.size(); i++ ) { diff --git a/plugins/Watsyn/Watsyn.cpp b/plugins/Watsyn/Watsyn.cpp index 7603a9c1be6..8e49942e16a 100644 --- a/plugins/Watsyn/Watsyn.cpp +++ b/plugins/Watsyn/Watsyn.cpp @@ -445,8 +445,6 @@ void WatsynInstrument::playNote( NotePlayHandle * _n, } applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Xpressive/Xpressive.cpp b/plugins/Xpressive/Xpressive.cpp index b1a17a1ce76..babc372317a 100644 --- a/plugins/Xpressive/Xpressive.cpp +++ b/plugins/Xpressive/Xpressive.cpp @@ -233,8 +233,6 @@ void Xpressive::playNote(NotePlayHandle* nph, sampleFrame* working_buffer) { const f_cnt_t offset = nph->noteOffset(); ps->renderOutput(frames, working_buffer + offset); - - instrumentTrack()->processAudioBuffer(working_buffer, frames + offset, nph); } void Xpressive::deleteNotePluginData(NotePlayHandle* nph) { diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index 2ec86459281..be38bcb7978 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -341,7 +341,6 @@ void ZynAddSubFxInstrument::play( sampleFrame * _buf ) m_plugin->processAudio( _buf ); } m_pluginMutex.unlock(); - instrumentTrack()->processAudioBuffer( _buf, Engine::audioEngine()->framesPerPeriod(), nullptr ); } diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 21a9a3598bc..29c54647cf7 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -333,12 +333,9 @@ void AudioEngine::pushInputFrames( sampleFrame * _ab, const f_cnt_t _frames ) - -const surroundSampleFrame * AudioEngine::renderNextBuffer() +void AudioEngine::renderStageNoteSetup() { - m_profiler.startPeriod(); - - s_renderingThread = true; + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::NoteSetup); if( m_clearSignal ) { @@ -387,9 +384,15 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() m_newPlayHandles.free( e ); e = next; } +} + + - // STAGE 1: run and render all play handles - AudioEngineWorkerThread::fillJobQueue( m_playHandles ); +void AudioEngine::renderStageInstruments() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Instruments); + + AudioEngineWorkerThread::fillJobQueue(m_playHandles); AudioEngineWorkerThread::startAndWaitForJobs(); // removed all play handles which are done @@ -417,15 +420,27 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() ++it; } } +} + + + +void AudioEngine::renderStageEffects() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Effects); // STAGE 2: process effects of all instrument- and sampletracks AudioEngineWorkerThread::fillJobQueue(m_audioPorts); AudioEngineWorkerThread::startAndWaitForJobs(); +} - // STAGE 3: do master mix in mixer - mixer->masterMix(m_outputBufferWrite); +void AudioEngine::renderStageMix() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Mixing); + + Mixer *mixer = Engine::mixer(); + mixer->masterMix(m_outputBufferWrite); emit nextAudioBuffer(m_outputBufferRead); @@ -435,10 +450,22 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() EnvelopeAndLfoParameters::instances()->trigger(); Controller::triggerFrameCounter(); AutomatableModel::incrementPeriodCounter(); +} - s_renderingThread = false; - m_profiler.finishPeriod( processingSampleRate(), m_framesPerPeriod ); + +const surroundSampleFrame *AudioEngine::renderNextBuffer() +{ + m_profiler.startPeriod(); + s_renderingThread = true; + + renderStageNoteSetup(); // STAGE 0: clear old play handles and buffers, setup new play handles + renderStageInstruments(); // STAGE 1: run and render all play handles + renderStageEffects(); // STAGE 2: process effects of all instrument- and sampletracks + renderStageMix(); // STAGE 3: do master mix in mixer + + s_renderingThread = false; + m_profiler.finishPeriod(processingSampleRate(), m_framesPerPeriod); return m_outputBufferRead; } @@ -674,7 +701,10 @@ void AudioEngine::removeAudioPort(AudioPort * port) bool AudioEngine::addPlayHandle( PlayHandle* handle ) { - if( criticalXRuns() == false ) + // Only add play handles if we have the CPU capacity to process them. + // Instrument play handles are not added during playback, but when the + // associated instrument is created, so add those unconditionally. + if (handle->type() == PlayHandle::Type::InstrumentPlayHandle || !criticalXRuns()) { m_newPlayHandles.push( handle ); handle->audioPort()->addPlayHandle( handle ); diff --git a/src/core/AudioEngineProfiler.cpp b/src/core/AudioEngineProfiler.cpp index 9e05ff80afa..82a412cbb98 100644 --- a/src/core/AudioEngineProfiler.cpp +++ b/src/core/AudioEngineProfiler.cpp @@ -24,6 +24,8 @@ #include "AudioEngineProfiler.h" +#include + namespace lmms { @@ -38,10 +40,24 @@ AudioEngineProfiler::AudioEngineProfiler() : void AudioEngineProfiler::finishPeriod( sample_rate_t sampleRate, fpp_t framesPerPeriod ) { - int periodElapsed = m_periodTimer.elapsed(); + // Time taken to process all data and fill the audio buffer. + const unsigned int periodElapsed = m_periodTimer.elapsed(); + // Maximum time the processing can take before causing buffer underflow. Convert to us. + const uint64_t timeLimit = static_cast(1000000) * framesPerPeriod / sampleRate; + + // Compute new overall CPU load and apply exponential averaging. + // The result is used for overload detection in AudioEngine::criticalXRuns() + // → the weight of a new sample must be high enough to allow relatively fast changes! + const auto newCpuLoad = 100.f * periodElapsed / timeLimit; + m_cpuLoad = newCpuLoad * 0.1f + m_cpuLoad * 0.9f; - const float newCpuLoad = periodElapsed / 10000.0f * sampleRate / framesPerPeriod; - m_cpuLoad = std::clamp((newCpuLoad * 0.1f + m_cpuLoad * 0.9f), 0, 100); + // Compute detailed load analysis. Can use stronger averaging to get more stable readout. + for (std::size_t i = 0; i < DetailCount; i++) + { + const auto newLoad = 100.f * m_detailTime[i] / timeLimit; + const auto oldLoad = m_detailLoad[i].load(std::memory_order_relaxed); + m_detailLoad[i].store(newLoad * 0.05f + oldLoad * 0.95f, std::memory_order_relaxed); + } if( m_outputFile.isOpen() ) { diff --git a/src/core/AutomationClip.cpp b/src/core/AutomationClip.cpp index 906cb148c82..3b36f6b49b7 100644 --- a/src/core/AutomationClip.cpp +++ b/src/core/AutomationClip.cpp @@ -1106,16 +1106,16 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) { QMutexLocker m(&m_clipMutex); - if( m_timeMap.size() < 2 && numToGenerate > 0 ) + for (int i = 0; i < numToGenerate && it != m_timeMap.end(); ++i, ++it) { - it.value().setInTangent(0); - it.value().setOutTangent(0); - return; - } - - for( int i = 0; i < numToGenerate; i++ ) - { - if( it == m_timeMap.begin() ) + if (it + 1 == m_timeMap.end()) + { + // Previously, the last value's tangent was always set to 0. That logic was kept for both tangents + // of the last node + it.value().setInTangent(0); + it.value().setOutTangent(0); + } + else if (it == m_timeMap.begin()) { // On the first node there's no curve behind it, so we will only calculate the outTangent // and inTangent will be set to 0. @@ -1123,14 +1123,6 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) it.value().setInTangent(0); it.value().setOutTangent(tangent); } - else if( it+1 == m_timeMap.end() ) - { - // Previously, the last value's tangent was always set to 0. That logic was kept for both tangents - // of the last node - it.value().setInTangent(0); - it.value().setOutTangent(0); - return; - } else { // When we are in a node that is in the middle of two other nodes, we need to check if we @@ -1159,7 +1151,6 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) it.value().setOutTangent(outTangent); } } - it++; } } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 319882af2f9..1155f5e0d22 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -70,6 +70,7 @@ set(LMMS_SRCS core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp core/Scale.cpp + core/LmmsSemaphore.cpp core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp @@ -112,6 +113,7 @@ set(LMMS_SRCS core/lv2/Lv2SubPluginFeatures.cpp core/lv2/Lv2UridCache.cpp core/lv2/Lv2UridMap.cpp + core/lv2/Lv2Worker.cpp core/midi/MidiAlsaRaw.cpp core/midi/MidiAlsaSeq.cpp diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 6ad2f8526d8..8d0a8dca43f 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -79,6 +79,7 @@ const std::vector DataFile::UPGRADE_METHODS = { &DataFile::upgrade_automationNodes , &DataFile::upgrade_extendedNoteRange, &DataFile::upgrade_defaultTripleOscillatorHQ, &DataFile::upgrade_mixerRename , &DataFile::upgrade_bbTcoRename, + &DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing }; // Vector of all versions that have upgrade routines. @@ -1671,6 +1672,8 @@ void DataFile::upgrade_extendedNoteRange() { auto root = documentElement(); UpgradeExtendedNoteRange upgradeExtendedNoteRange(root); + + upgradeExtendedNoteRange.upgrade(); } @@ -1701,26 +1704,56 @@ void DataFile::upgrade_mixerRename() { // Change nodename to QDomNodeList fxmixer = elementsByTagName("fxmixer"); - for (int i = 0; !fxmixer.item(i).isNull(); ++i) + for (int i = 0; i < fxmixer.length(); ++i) { - fxmixer.item(i).toElement().setTagName("mixer"); + auto item = fxmixer.item(i).toElement(); + if (item.isNull()) + { + continue; + } + item.setTagName("mixer"); } // Change nodename to QDomNodeList fxchannel = elementsByTagName("fxchannel"); - for (int i = 0; !fxchannel.item(i).isNull(); ++i) + for (int i = 0; i < fxchannel.length(); ++i) { - fxchannel.item(i).toElement().setTagName("mixerchannel"); + auto item = fxchannel.item(i).toElement(); + if (item.isNull()) + { + continue; + } + item.setTagName("mixerchannel"); } // Change the attribute fxch of element to mixch QDomNodeList fxch = elementsByTagName("instrumenttrack"); - for(int i = 0; !fxch.item(i).isNull(); ++i) + for (int i = 0; i < fxch.length(); ++i) + { + auto item = fxch.item(i).toElement(); + if (item.isNull()) + { + continue; + } + if (item.hasAttribute("fxch")) + { + item.setAttribute("mixch", item.attribute("fxch")); + item.removeAttribute("fxch"); + } + } + // Change the attribute fxch of element to mixch + fxch = elementsByTagName("sampletrack"); + for (int i = 0; i < fxch.length(); ++i) { - if(fxch.item(i).toElement().hasAttribute("fxch")) + auto item = fxch.item(i).toElement(); + if (item.isNull()) + { + continue; + } + if (item.hasAttribute("fxch")) { - fxch.item(i).toElement().setAttribute("mixch", fxch.item(i).toElement().attribute("fxch")); - fxch.item(i).toElement().removeAttribute("fxch"); + item.setAttribute("mixch", item.attribute("fxch")); + item.removeAttribute("fxch"); } } } @@ -1760,6 +1793,44 @@ void DataFile::upgrade_bbTcoRename() } +// Set LFO speed to 0.01 on projects made before sample-and-hold PR +void DataFile::upgrade_sampleAndHold() +{ + QDomNodeList elements = elementsByTagName("lfocontroller"); + for (int i = 0; i < elements.length(); ++i) + { + if (elements.item(i).isNull()) { continue; } + auto e = elements.item(i).toElement(); + // Correct old random wave LFO speeds + if (e.attribute("wave").toInt() == 6) + { + e.setAttribute("speed",0.01f); + } + } +} + +//! Update MIDI CC indexes, so that they are counted from 0. Older releases of LMMS +//! count the CCs from 1. +void DataFile::upgrade_midiCCIndexing() +{ + static constexpr std::array attributesToUpdate{"inputcontroller", "outputcontroller"}; + + QDomNodeList elements = elementsByTagName("Midicontroller"); + for(int i = 0; i < elements.length(); i++) + { + if (elements.item(i).isNull()) { continue; } + auto element = elements.item(i).toElement(); + for (const char* attrName : attributesToUpdate) + { + if (element.hasAttribute(attrName)) + { + int cc = element.attribute(attrName).toInt(); + element.setAttribute(attrName, cc - 1); + } + } + } +} + void DataFile::upgrade() { // Runs all necessary upgrade methods diff --git a/src/core/EffectChain.cpp b/src/core/EffectChain.cpp index b07a7227bd9..4da5c519787 100644 --- a/src/core/EffectChain.cpp +++ b/src/core/EffectChain.cpp @@ -25,6 +25,7 @@ #include +#include #include "EffectChain.h" #include "Effect.h" @@ -162,6 +163,7 @@ void EffectChain::moveDown( Effect * _effect ) if (_effect != m_effects.back()) { auto it = std::find(m_effects.begin(), m_effects.end(), _effect); + assert(it != m_effects.end()); std::swap(*std::next(it), *it); } } @@ -174,6 +176,7 @@ void EffectChain::moveUp( Effect * _effect ) if (_effect != m_effects.front()) { auto it = std::find(m_effects.begin(), m_effects.end(), _effect); + assert(it != m_effects.end()); std::swap(*std::prev(it), *it); } } diff --git a/src/core/InstrumentPlayHandle.cpp b/src/core/InstrumentPlayHandle.cpp index e1a9d9d65fd..097719ad83d 100644 --- a/src/core/InstrumentPlayHandle.cpp +++ b/src/core/InstrumentPlayHandle.cpp @@ -24,18 +24,57 @@ #include "InstrumentPlayHandle.h" +#include "Instrument.h" #include "InstrumentTrack.h" +#include "Engine.h" +#include "AudioEngine.h" namespace lmms { -InstrumentPlayHandle::InstrumentPlayHandle( Instrument * instrument, InstrumentTrack* instrumentTrack ) : - PlayHandle( Type::InstrumentPlayHandle ), - m_instrument( instrument ) +InstrumentPlayHandle::InstrumentPlayHandle(Instrument * instrument, InstrumentTrack* instrumentTrack) : + PlayHandle(Type::InstrumentPlayHandle), + m_instrument(instrument) { - setAudioPort( instrumentTrack->audioPort() ); + setAudioPort(instrumentTrack->audioPort()); +} + +void InstrumentPlayHandle::play(sampleFrame * working_buffer) +{ + InstrumentTrack * instrumentTrack = m_instrument->instrumentTrack(); + + // ensure that all our nph's have been processed first + auto nphv = NotePlayHandle::nphsOfInstrumentTrack(instrumentTrack, true); + + bool nphsLeft; + do + { + nphsLeft = false; + for (const NotePlayHandle * constNotePlayHandle : nphv) + { + if (constNotePlayHandle->state() != ThreadableJob::ProcessingState::Done && + !constNotePlayHandle->isFinished()) + { + nphsLeft = true; + NotePlayHandle * notePlayHandle = const_cast(constNotePlayHandle); + notePlayHandle->process(); + } + } + } + while (nphsLeft); + + m_instrument->play(working_buffer); + + // Process the audio buffer that the instrument has just worked on... + const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); + instrumentTrack->processAudioBuffer(working_buffer, frames, nullptr); +} + +bool InstrumentPlayHandle::isFromTrack(const Track* track) const +{ + return m_instrument->isFromTrack(track); } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 23621b84727..88f64803c2b 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -88,6 +88,7 @@ void LfoController::updateValueBuffer() { m_phaseOffset = m_phaseModel.value() / 360.0; float phase = m_currentPhase + m_phaseOffset; + float phasePrev = 0.0f; // roll phase up until we're in sync with period counter m_bufferLastUpdated++; @@ -102,20 +103,45 @@ void LfoController::updateValueBuffer() ValueBuffer *amountBuffer = m_amountModel.valueBuffer(); int amountInc = amountBuffer ? 1 : 0; float *amountPtr = amountBuffer ? &(amountBuffer->values()[ 0 ] ) : &amount; + Oscillator::WaveShape waveshape = static_cast(m_waveModel.value()); for( float& f : m_valueBuffer ) { - const float currentSample = m_sampleFunction != nullptr - ? m_sampleFunction( phase ) - : m_userDefSampleBuffer->userWaveSample( phase ); + float currentSample = 0; + switch (waveshape) + { + case Oscillator::WaveShape::WhiteNoise: + { + if (absFraction(phase) < absFraction(phasePrev)) + { + // Resample when phase period has completed + m_heldSample = m_sampleFunction(phase); + } + currentSample = m_heldSample; + break; + } + case Oscillator::WaveShape::UserDefined: + { + currentSample = m_userDefSampleBuffer->userWaveSample(phase); + break; + } + default: + { + if (m_sampleFunction != nullptr) + { + currentSample = m_sampleFunction(phase); + } + } + } f = std::clamp(m_baseModel.value() + (*amountPtr * currentSample / 2.0f), 0.0f, 1.0f); + phasePrev = phase; phase += 1.0 / m_duration; amountPtr += amountInc; } - m_currentPhase = absFraction( phase - m_phaseOffset ); + m_currentPhase = absFraction(phase - m_phaseOffset); m_bufferLastUpdated = s_periods; } diff --git a/src/core/LmmsSemaphore.cpp b/src/core/LmmsSemaphore.cpp new file mode 100644 index 00000000000..daa70a45ba3 --- /dev/null +++ b/src/core/LmmsSemaphore.cpp @@ -0,0 +1,143 @@ +/* + * Semaphore.cpp - Semaphore implementation + * + * Copyright (c) 2022-2022 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. + * + */ + +/* + * This code has been copied and adapted from https://github.com/drobilla/jalv + * File src/zix/sem.h + */ + +#include "LmmsSemaphore.h" + +#if defined(LMMS_BUILD_WIN32) +# include +#else +# include +#endif + +#include + +namespace lmms { + +#ifdef LMMS_BUILD_APPLE +Semaphore::Semaphore(unsigned val) +{ + kern_return_t rval = semaphore_create(mach_task_self(), &m_sem, SYNC_POLICY_FIFO, val); + if(rval != 0) { + throw std::system_error(rval, std::system_category(), "Could not create semaphore"); + } +} + +Semaphore::~Semaphore() +{ + semaphore_destroy(mach_task_self(), m_sem); +} + +void Semaphore::post() +{ + semaphore_signal(m_sem); +} + +void Semaphore::wait() +{ + kern_return_t rval = semaphore_wait(m_sem); + if (rval != KERN_SUCCESS) { + throw std::system_error(rval, std::system_category(), "Waiting for semaphore failed"); + } +} + +bool Semaphore::tryWait() +{ + const mach_timespec_t zero = { 0, 0 }; + return semaphore_timedwait(m_sem, zero) == KERN_SUCCESS; +} + +#elif defined(LMMS_BUILD_WIN32) + +Semaphore::Semaphore(unsigned initial) +{ + if(CreateSemaphore(nullptr, initial, LONG_MAX, nullptr) == nullptr) { + throw std::system_error(GetLastError(), std::system_category(), "Could not create semaphore"); + } +} + +Semaphore::~Semaphore() +{ + CloseHandle(m_sem); +} + +void Semaphore::post() +{ + ReleaseSemaphore(m_sem, 1, nullptr); +} + +void Semaphore::wait() +{ + if (WaitForSingleObject(m_sem, INFINITE) != WAIT_OBJECT_0) { + throw std::system_error(GetLastError(), std::system_category(), "Waiting for semaphore failed"); + } +} + +bool Semaphore::tryWait() +{ + return WaitForSingleObject(m_sem, 0) == WAIT_OBJECT_0; +} + +#else /* !defined(LMMS_BUILD_APPLE) && !defined(LMMS_BUILD_WIN32) */ + +Semaphore::Semaphore(unsigned initial) +{ + if(sem_init(&m_sem, 0, initial) != 0) { + throw std::system_error(errno, std::generic_category(), "Could not create semaphore"); + } +} + +Semaphore::~Semaphore() +{ + sem_destroy(&m_sem); +} + +void Semaphore::post() +{ + sem_post(&m_sem); +} + +void Semaphore::wait() +{ + while (sem_wait(&m_sem) != 0) { + if (errno != EINTR) { + throw std::system_error(errno, std::generic_category(), "Waiting for semaphore failed"); + } + /* Otherwise, interrupted, so try again. */ + } +} + +bool Semaphore::tryWait() +{ + return (sem_trywait(&m_sem) == 0); +} + +#endif + +} // namespace lmms + diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index e14660e1fa3..59c2dd72e16 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -394,8 +394,8 @@ void Mixer::moveChannelLeft( int index ) else if (m_lastSoloed == b) { m_lastSoloed = a; } // go through every instrument and adjust for the channel index change - TrackContainer::TrackList songTrackList = Engine::getSong()->tracks(); - TrackContainer::TrackList patternTrackList = Engine::patternStore()->tracks(); + const TrackContainer::TrackList& songTrackList = Engine::getSong()->tracks(); + const TrackContainer::TrackList& patternTrackList = Engine::patternStore()->tracks(); for (const auto& trackList : {songTrackList, patternTrackList}) { diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index 70007ebf187..eb9c7ddbff4 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -557,14 +557,20 @@ void NotePlayHandle::updateFrequency() -void NotePlayHandle::processTimePos( const TimePos& time ) +void NotePlayHandle::processTimePos(const TimePos& time, float pitchValue, bool isRecording) { - if( detuning() && time >= songGlobalParentOffset()+pos() ) + if (!detuning() || time < songGlobalParentOffset() + pos()) { return; } + + if (isRecording && m_origin == Origin::MidiInput) + { + detuning()->automationClip()->recordValue(time - songGlobalParentOffset() - pos(), pitchValue / 100); + } + else { - const float v = detuning()->automationClip()->valueAt( time - songGlobalParentOffset() - pos() ); - if( !typeInfo::isEqual( v, m_baseDetuning->value() ) ) + const float v = detuning()->automationClip()->valueAt(time - songGlobalParentOffset() - pos()); + if (!typeInfo::isEqual(v, m_baseDetuning->value())) { - m_baseDetuning->setValue( v ); + m_baseDetuning->setValue(v); updateFrequency(); } } diff --git a/src/core/PatternStore.cpp b/src/core/PatternStore.cpp index c5a3521398a..6af434f65b3 100644 --- a/src/core/PatternStore.cpp +++ b/src/core/PatternStore.cpp @@ -61,7 +61,7 @@ bool PatternStore::play(TimePos start, fpp_t frames, f_cnt_t offset, int clipNum start = start % (lengthOfPattern(clipNum) * TimePos::ticksPerBar()); - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { if (t->play(start, frames, offset, clipNum)) @@ -117,7 +117,7 @@ int PatternStore::numOfPatterns() const void PatternStore::removePattern(int pattern) { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { delete t->getClip(pattern); @@ -134,7 +134,7 @@ void PatternStore::removePattern(int pattern) void PatternStore::swapPattern(int pattern1, int pattern2) { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { t->swapPositionOfClips(pattern1, pattern2); @@ -159,7 +159,7 @@ void PatternStore::updatePatternTrack(Clip* clip) void PatternStore::fixIncorrectPositions() { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { for (int i = 0; i < numOfPatterns(); ++i) @@ -215,7 +215,7 @@ void PatternStore::updateComboBox() void PatternStore::currentPatternChanged() { // now update all track-labels (the current one has to become white, the others gray) - TrackList tl = Engine::getSong()->tracks(); + const TrackList& tl = Engine::getSong()->tracks(); for (Track * t : tl) { if (t->type() == Track::Type::Pattern) @@ -230,7 +230,7 @@ void PatternStore::currentPatternChanged() void PatternStore::createClipsForPattern(int pattern) { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { t->createClipsForPattern(pattern); diff --git a/src/core/RenderManager.cpp b/src/core/RenderManager.cpp index 969cad15bed..9f619203903 100644 --- a/src/core/RenderManager.cpp +++ b/src/core/RenderManager.cpp @@ -97,7 +97,7 @@ void RenderManager::renderNextTrack() // Render the song into individual tracks void RenderManager::renderTracks() { - const TrackContainer::TrackList & tl = Engine::getSong()->tracks(); + const TrackContainer::TrackList& tl = Engine::getSong()->tracks(); // find all currently unnmuted tracks -- we want to render these. for (const auto& tk : tl) @@ -112,7 +112,7 @@ void RenderManager::renderTracks() } } - const TrackContainer::TrackList t2 = Engine::patternStore()->tracks(); + const TrackContainer::TrackList& t2 = Engine::patternStore()->tracks(); for (const auto& tk : t2) { Track::Type type = tk->type(); diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 775db125b75..5e2d09c573e 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -247,7 +247,15 @@ void SampleBuffer::update(bool keepSettings) const int fileSizeMax = 300; // MB const int sampleLengthMax = 90; // Minutes - bool fileLoadError = false; + enum class FileLoadError + { + None, + ReadPermissionDenied, + TooLarge, + Invalid + }; + FileLoadError fileLoadError = FileLoadError::None; + if (m_audioFile.isEmpty() && m_origData != nullptr && m_origFrames > 0) { // TODO: reverse- and amplification-property is not covered @@ -271,31 +279,40 @@ void SampleBuffer::update(bool keepSettings) m_frames = 0; const QFileInfo fileInfo(file); - if (fileInfo.size() > fileSizeMax * 1024 * 1024) + if (!fileInfo.isReadable()) { - fileLoadError = true; + fileLoadError = FileLoadError::ReadPermissionDenied; + } + else if (fileInfo.size() > fileSizeMax * 1024 * 1024) + { + fileLoadError = FileLoadError::TooLarge; } else { // Use QFile to handle unicode file names on Windows QFile f(file); - SNDFILE * sndFile; + SNDFILE * sndFile = nullptr; SF_INFO sfInfo; sfInfo.format = 0; + if (f.open(QIODevice::ReadOnly) && (sndFile = sf_open_fd(f.handle(), SFM_READ, &sfInfo, false))) { f_cnt_t frames = sfInfo.frames; int rate = sfInfo.samplerate; if (frames / rate > sampleLengthMax * 60) { - fileLoadError = true; + fileLoadError = FileLoadError::TooLarge; } sf_close(sndFile); } + else + { + fileLoadError = FileLoadError::Invalid; + } f.close(); } - if (!fileLoadError) + if (fileLoadError == FileLoadError::None) { #ifdef LMMS_HAVE_OGGVORBIS // workaround for a bug in libsndfile or our libsndfile decoder @@ -322,7 +339,7 @@ void SampleBuffer::update(bool keepSettings) } } - if (m_frames == 0 || fileLoadError) // if still no frames, bail + if (m_frames == 0 || fileLoadError != FileLoadError::None) // if still no frames, bail { // sample couldn't be decoded, create buffer containing // one sample-frame @@ -363,16 +380,35 @@ void SampleBuffer::update(bool keepSettings) } Oscillator::generateAntiAliasUserWaveTable(this); - if (fileLoadError) + if (fileLoadError != FileLoadError::None) { QString title = tr("Fail to open file"); - QString message = tr("Audio files are limited to %1 MB " - "in size and %2 minutes of playing time" - ).arg(fileSizeMax).arg(sampleLengthMax); + QString message; + + switch (fileLoadError) + { + case FileLoadError::None: + // present just to avoid a compiler warning + break; + + case FileLoadError::ReadPermissionDenied: + message = tr("Read permission denied"); + break; + + case FileLoadError::TooLarge: + message = tr("Audio files are limited to %1 MB " + "in size and %2 minutes of playing time" + ).arg(fileSizeMax).arg(sampleLengthMax); + break; + + case FileLoadError::Invalid: + message = tr("Invalid audio file"); + break; + } + if (gui::getGUI() != nullptr) { - QMessageBox::information(nullptr, - title, message, QMessageBox::Ok); + QMessageBox::information(nullptr, title, message, QMessageBox::Ok); } else { diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 592a6382741..b09d7b3bb2b 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -143,23 +143,26 @@ void SampleClip::setSampleBuffer( SampleBuffer* sb ) -void SampleClip::setSampleFile( const QString & _sf ) +void SampleClip::setSampleFile(const QString & sf) { - int length; - if ( _sf.isEmpty() ) - { //When creating an empty sample clip make it a bar long + int length = 0; + + if (!sf.isEmpty()) + { + m_sampleBuffer->setAudioFile(sf); + length = sampleLength(); + } + + if (length == 0) + { + //If there is no sample, make the clip a bar long float nom = Engine::getSong()->getTimeSigModel().getNumerator(); float den = Engine::getSong()->getTimeSigModel().getDenominator(); - length = DefaultTicksPerBar * ( nom / den ); + length = DefaultTicksPerBar * (nom / den); } - else - { //Otherwise set it to the sample's length - m_sampleBuffer->setAudioFile( _sf ); - length = sampleLength(); - } - changeLength(length); - setStartTimeOffset( 0 ); + changeLength(length); + setStartTimeOffset(0); emit sampleChanged(); emit playbackPositionChanged(); diff --git a/src/core/Song.cpp b/src/core/Song.cpp index e8073f225f9..3a735331c64 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -383,7 +383,7 @@ void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp } values = container->automatedValuesAt(timeStart, clipNum); - TrackList tracks = container->tracks(); + const TrackList& tracks = container->tracks(); Track::clipVector clips; for (Track* track : tracks) diff --git a/src/core/UpgradeExtendedNoteRange.cpp b/src/core/UpgradeExtendedNoteRange.cpp index e61da3723c4..6ed98e63e9e 100644 --- a/src/core/UpgradeExtendedNoteRange.cpp +++ b/src/core/UpgradeExtendedNoteRange.cpp @@ -193,6 +193,7 @@ static void fixTrack(QDomElement & track, std::set & automatedBase for (int i = 0; i < subTracks.size(); ++i) { QDomElement subTrack = subTracks.item(i).toElement(); + assert (static_cast(subTrack.attribute("type").toInt()) != Track::Type::Pattern); fixTrack(subTrack, automatedBaseNoteIds); } } diff --git a/src/core/lv2/Lv2Features.cpp b/src/core/lv2/Lv2Features.cpp index 6e74a89360c..c8fc0546517 100644 --- a/src/core/lv2/Lv2Features.cpp +++ b/src/core/lv2/Lv2Features.cpp @@ -48,7 +48,7 @@ Lv2Features::Lv2Features() { const Lv2Manager* man = Engine::getLv2Manager(); // create (yet empty) map feature URI -> feature - for(const char* uri : man->supportedFeatureURIs()) + for(auto uri : man->supportedFeatureURIs()) { m_featureByUri.emplace(uri, nullptr); } @@ -71,7 +71,7 @@ void Lv2Features::initCommon() void Lv2Features::createFeatureVectors() { // create vector of features - for(std::pair& pr : m_featureByUri) + for(const auto& [uri, feature] : m_featureByUri) { /* If pr.second is nullptr here, this means that the LV2_feature @@ -82,7 +82,7 @@ void Lv2Features::createFeatureVectors() vector creation (This can be done in Lv2Proc::initPluginSpecificFeatures or in Lv2Features::initCommon) */ - m_features.push_back(LV2_Feature { pr.first, pr.second }); + m_features.push_back(LV2_Feature{(const char*)uri.data(), (void*)feature}); } // create pointer vector (for lilv_plugin_instantiate) diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index 9c62703e0a4..6a1b2a8af20 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -28,13 +28,14 @@ #include #include -#include #include #include #include +#include #include #include +#include "AudioEngine.h" #include "Engine.h" #include "Plugin.h" #include "Lv2ControlBase.h" @@ -46,7 +47,7 @@ namespace lmms { -const std::set Lv2Manager::pluginBlacklist = +const std::set Lv2Manager::pluginBlacklist = { // github.com/calf-studio-gear/calf, #278 "http://calf.sourceforge.net/plugins/Analyzer", @@ -137,6 +138,26 @@ const std::set Lv2Manager::pluginBlacklist = "urn:juced:DrumSynth" }; +const std::set Lv2Manager::pluginBlacklistBuffersizeLessThan32 = +{ + "http://moddevices.com/plugins/mod-devel/2Voices", + "http://moddevices.com/plugins/mod-devel/Capo", + "http://moddevices.com/plugins/mod-devel/Drop", + "http://moddevices.com/plugins/mod-devel/Harmonizer", + "http://moddevices.com/plugins/mod-devel/Harmonizer2", + "http://moddevices.com/plugins/mod-devel/HarmonizerCS", + "http://moddevices.com/plugins/mod-devel/SuperCapo", + "http://moddevices.com/plugins/mod-devel/SuperWhammy", + "http://moddevices.com/plugins/mod-devel/Gx2Voices", + "http://moddevices.com/plugins/mod-devel/GxCapo", + "http://moddevices.com/plugins/mod-devel/GxDrop", + "http://moddevices.com/plugins/mod-devel/GxHarmonizer", + "http://moddevices.com/plugins/mod-devel/GxHarmonizer2", + "http://moddevices.com/plugins/mod-devel/GxHarmonizerCS", + "http://moddevices.com/plugins/mod-devel/GxSuperCapo", + "http://moddevices.com/plugins/mod-devel/GxSuperWhammy" +}; + @@ -152,10 +173,15 @@ Lv2Manager::Lv2Manager() : m_supportedFeatureURIs.insert(LV2_URID__map); m_supportedFeatureURIs.insert(LV2_URID__unmap); m_supportedFeatureURIs.insert(LV2_OPTIONS__options); + m_supportedFeatureURIs.insert(LV2_WORKER__schedule); // min/max is always passed in the options m_supportedFeatureURIs.insert(LV2_BUF_SIZE__boundedBlockLength); // block length is only changed initially in AudioEngine CTOR m_supportedFeatureURIs.insert(LV2_BUF_SIZE__fixedBlockLength); + if (const auto fpp = Engine::audioEngine()->framesPerPeriod(); (fpp & (fpp - 1)) == 0) // <=> ffp is power of 2 (for ffp > 0) + { + m_supportedFeatureURIs.insert(LV2_BUF_SIZE__powerOf2BlockLength); + } auto supportOpt = [this](Lv2UridCache::Id id) { @@ -288,14 +314,6 @@ void Lv2Manager::initPlugins() -bool Lv2Manager::CmpStr::operator()(const char *a, const char *b) const -{ - return std::strcmp(a, b) < 0; -} - - - - bool Lv2Manager::isFeatureSupported(const char *featName) const { return m_supportedFeatureURIs.find(featName) != m_supportedFeatureURIs.end(); diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index e0541b948c9..11290013e5b 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -1,7 +1,7 @@ /* * Lv2Proc.cpp - Lv2 processor class * - * Copyright (c) 2019-2020 Johannes Lorenz + * Copyright (c) 2019-2022 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -75,11 +76,19 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin, // TODO: manage a global blacklist outside of the code // for now, this will help // this is only a fix for the meantime - const auto& pluginBlacklist = Lv2Manager::getPluginBlacklist(); - if (!Engine::ignorePluginBlacklist() && - pluginBlacklist.find(pluginUri) != pluginBlacklist.end()) + if (!Engine::ignorePluginBlacklist()) { - issues.emplace_back(PluginIssueType::Blacklisted); + const auto& pluginBlacklist = Lv2Manager::getPluginBlacklist(); + const auto& pluginBlacklist32 = Lv2Manager::getPluginBlacklistBuffersizeLessThan32(); + if(pluginBlacklist.find(pluginUri) != pluginBlacklist.end()) + { + issues.emplace_back(PluginIssueType::Blacklisted); + } + else if(Engine::audioEngine()->framesPerPeriod() <= 32 && + pluginBlacklist32.find(pluginUri) != pluginBlacklist32.end()) + { + issues.emplace_back(PluginIssueType::Blacklisted); // currently no special blacklist category + } } for (unsigned portNum = 0; portNum < maxPorts; ++portNum) @@ -162,6 +171,7 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin, Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : LinkedModelGroup(parent), m_plugin(plugin), + m_workLock(1), m_midiInputBuf(m_maxMidiInputEvents), m_midiInputReader(m_midiInputBuf) { @@ -352,7 +362,19 @@ void Lv2Proc::copyBuffersToCore(sampleFrame* buf, void Lv2Proc::run(fpp_t frames) { + if (m_worker) + { + // Process any worker replies + m_worker->emitResponses(); + } + lilv_instance_run(m_instance, static_cast(frames)); + + if (m_worker) + { + // Notify the plugin the run() cycle is finished + m_worker->notifyPluginThatRunFinished(); + } } @@ -420,6 +442,9 @@ void Lv2Proc::initPlugin() if (m_instance) { + if(m_worker) { + m_worker->setHandle(lilv_instance_get_handle(m_instance)); + } for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum) connectPort(portNum); lilv_instance_activate(m_instance); @@ -496,8 +521,20 @@ void Lv2Proc::initMOptions() void Lv2Proc::initPluginSpecificFeatures() { + // options initMOptions(); m_features[LV2_OPTIONS__options] = const_cast(m_options.feature()); + + // worker (if plugin has worker extension) + Lv2Manager* mgr = Engine::getLv2Manager(); + if (lilv_plugin_has_extension_data(m_plugin, mgr->uri(LV2_WORKER__interface).get())) { + const auto iface = static_cast( + lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface)); + bool threaded = !Engine::audioEngine()->renderOnly(); + m_worker.emplace(iface, &m_workLock, threaded); + m_features[LV2_WORKER__schedule] = m_worker->feature(); + // Note: m_worker::setHandle will still need to be called later + } } @@ -566,21 +603,35 @@ void Lv2Proc::createPort(std::size_t portNum) break; case Lv2Ports::Vis::Enumeration: { - auto comboModel = new ComboBoxModel(nullptr, dispName); - LilvScalePoints* sps = - lilv_port_get_scale_points(m_plugin, lilvPort); - LILV_FOREACH(scale_points, i, sps) + ComboBoxModel* comboModel = new ComboBoxModel(nullptr, dispName); + + { + AutoLilvScalePoints sps (static_cast(lilv_port_get_scale_points(m_plugin, lilvPort))); + // temporary map, since lilv may return scale points in random order + std::map scalePointMap; + LILV_FOREACH(scale_points, i, sps.get()) + { + const LilvScalePoint* sp = lilv_scale_points_get(sps.get(), i); + const float f = lilv_node_as_float(lilv_scale_point_get_value(sp)); + const char* s = lilv_node_as_string(lilv_scale_point_get_label(sp)); + scalePointMap[f] = s; + } + for (const auto& [f,s] : scalePointMap) + { + ctrl->m_scalePointMap.push_back(f); + comboModel->addItem(s); + } + } + for(std::size_t i = 0; i < ctrl->m_scalePointMap.size(); ++i) { - const LilvScalePoint* sp = lilv_scale_points_get(sps, i); - ctrl->m_scalePointMap.push_back(lilv_node_as_float( - lilv_scale_point_get_value(sp))); - comboModel->addItem( - lilv_node_as_string( - lilv_scale_point_get_label(sp))); + if(meta.def() == ctrl->m_scalePointMap[i]) + { + comboModel->setValue(i); + comboModel->setInitValue(i); + break; + } } - lilv_scale_points_free(sps); ctrl->m_connectedModel.reset(comboModel); - // TODO: use default value on comboModel, too? break; } case Lv2Ports::Vis::Toggled: diff --git a/src/core/lv2/Lv2Worker.cpp b/src/core/lv2/Lv2Worker.cpp new file mode 100644 index 00000000000..5af955ff766 --- /dev/null +++ b/src/core/lv2/Lv2Worker.cpp @@ -0,0 +1,203 @@ +/* + * Lv2Worker.cpp - Lv2Worker implementation + * + * Copyright (c) 2022-2022 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 "Lv2Worker.h" + +#include +#include + +#ifdef LMMS_HAVE_LV2 + +#include "Engine.h" + + +namespace lmms +{ + + +// static wrappers + +static LV2_Worker_Status +staticWorkerRespond(LV2_Worker_Respond_Handle handle, + uint32_t size, const void* data) +{ + Lv2Worker* worker = static_cast(handle); + return worker->respond(size, data); +} + + + + +std::size_t Lv2Worker::bufferSize() const +{ + // ardour uses this fixed size for ALSA: + return 8192 * 4; + // for jack, they use 4 * jack_port_type_get_buffer_size (..., JACK_DEFAULT_MIDI_TYPE) + // (possible extension for AudioDevice) +} + + + + +Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, + Semaphore* common_work_lock, + bool threaded) : + m_iface(iface), + m_threaded(threaded), + m_response(bufferSize()), + m_requests(bufferSize()), + m_responses(bufferSize()), + m_requestsReader(m_requests), + m_responsesReader(m_responses), + m_sem(0), + m_workLock(common_work_lock) +{ + assert(iface); + m_scheduleFeature.handle = static_cast(this); + m_scheduleFeature.schedule_work = [](LV2_Worker_Schedule_Handle handle, + uint32_t size, const void* data) -> LV2_Worker_Status + { + Lv2Worker* worker = static_cast(handle); + return worker->scheduleWork(size, data); + }; + + if (threaded) { m_thread = std::thread(&Lv2Worker::workerFunc, this); } + + m_requests.mlock(); + m_responses.mlock(); +} + + + + +Lv2Worker::~Lv2Worker() +{ + m_exit = true; + if(m_threaded) { + m_sem.post(); + m_thread.join(); + } +} + + + + +// Let the worker send responses to the audio thread +LV2_Worker_Status Lv2Worker::respond(uint32_t size, const void* data) +{ + if(m_threaded) + { + if(m_responses.free() < sizeof(size) + size) + { + return LV2_WORKER_ERR_NO_SPACE; + } + else + { + m_responses.write((const char*)&size, sizeof(size)); + if(size && data) { m_responses.write((const char*)data, size); } + } + } + else + { + m_iface->work_response(m_handle, size, data); + } + return LV2_WORKER_SUCCESS; +} + + + + +// Let the worker receive work from the audio thread and "work" on it +void Lv2Worker::workerFunc() +{ + std::vector buf; + uint32_t size; + while (true) { + m_sem.wait(); + if (m_exit) { break; } + const std::size_t readSpace = m_requestsReader.read_space(); + if (readSpace <= sizeof(size)) { continue; } // (should not happen) + + m_requestsReader.read(sizeof(size)).copy((char*)&size, sizeof(size)); + assert(size <= readSpace - sizeof(size)); + if(size > buf.size()) { buf.resize(size); } + if(size) { m_requestsReader.read(size).copy(buf.data(), size); } + + m_workLock->wait(); + m_iface->work(m_handle, staticWorkerRespond, this, size, buf.data()); + m_workLock->post(); + } +} + + + + +// Let the audio thread schedule work for the worker +LV2_Worker_Status Lv2Worker::scheduleWork(uint32_t size, const void *data) +{ + if (m_threaded) + { + if(m_requests.free() < sizeof(size) + size) + { + return LV2_WORKER_ERR_NO_SPACE; + } + else + { + // Schedule a request to be executed by the worker thread + m_requests.write((const char*)&size, sizeof(size)); + if(size && data) { m_requests.write((const char*)data, size); } + m_sem.post(); + } + } + else + { + // Execute work immediately in this thread + m_workLock->wait(); + m_iface->work(m_handle, staticWorkerRespond, this, size, data); + m_workLock->post(); + } + + return LV2_WORKER_SUCCESS; +} + + + + +// Let the audio thread read incoming worker responses, and process it +void Lv2Worker::emitResponses() +{ + std::size_t read_space = m_responsesReader.read_space(); + uint32_t size; + while (read_space > sizeof(size)) { + m_responsesReader.read(sizeof(size)).copy((char*)&size, sizeof(size)); + if(size) { m_responsesReader.read(size).copy(m_response.data(), size); } + m_iface->work_response(m_handle, size, m_response.data()); + read_space -= sizeof(size) + size; + } +} + + +} // namespace lmms + +#endif // LMMS_HAVE_LV2 diff --git a/src/core/midi/MidiController.cpp b/src/core/midi/MidiController.cpp index d7c89e940c2..0ae76d352c8 100644 --- a/src/core/midi/MidiController.cpp +++ b/src/core/midi/MidiController.cpp @@ -72,21 +72,20 @@ void MidiController::updateName() -void MidiController::processInEvent( const MidiEvent& event, const TimePos& time, f_cnt_t offset ) +void MidiController::processInEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) { unsigned char controllerNum; - switch( event.type() ) + switch(event.type()) { case MidiControlChange: controllerNum = event.controllerNumber(); - if( m_midiPort.inputController() == controllerNum + 1 && - ( m_midiPort.inputChannel() == event.channel() + 1 || - m_midiPort.inputChannel() == 0 ) ) + if (m_midiPort.inputController() == controllerNum && + (m_midiPort.inputChannel() == event.channel() + 1 || m_midiPort.inputChannel() == 0)) { unsigned char val = event.controllerValue(); m_previousValue = m_lastValue; - m_lastValue = (float)( val ) / 127.0f; + m_lastValue = static_cast(val) / 127.0f; emit valueChanged(); } break; diff --git a/src/core/midi/MidiPort.cpp b/src/core/midi/MidiPort.cpp index c7c947e8e4f..24263f91315 100644 --- a/src/core/midi/MidiPort.cpp +++ b/src/core/midi/MidiPort.cpp @@ -31,6 +31,7 @@ #include "MidiEventProcessor.h" #include "Note.h" #include "Song.h" +#include "MidiController.h" namespace lmms @@ -54,8 +55,8 @@ MidiPort::MidiPort( const QString& name, m_mode( mode ), m_inputChannelModel( 0, 0, MidiChannelCount, this, tr( "Input channel" ) ), m_outputChannelModel( 1, 0, MidiChannelCount, this, tr( "Output channel" ) ), - m_inputControllerModel( 0, 0, MidiControllerCount, this, tr( "Input controller" ) ), - m_outputControllerModel( 0, 0, MidiControllerCount, this, tr( "Output controller" ) ), + m_inputControllerModel(MidiController::NONE, MidiController::NONE, MidiControllerCount - 1, this, tr( "Input controller" )), + m_outputControllerModel(MidiController::NONE, MidiController::NONE, MidiControllerCount - 1, this, tr( "Output controller" )), m_fixedInputVelocityModel( -1, -1, MidiMaxVelocity, this, tr( "Fixed input velocity" ) ), m_fixedOutputVelocityModel( -1, -1, MidiMaxVelocity, this, tr( "Fixed output velocity" ) ), m_fixedOutputNoteModel( -1, -1, MidiMaxKey, this, tr( "Fixed output note" ) ), @@ -436,4 +437,4 @@ void MidiPort::invalidateCilent() } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9f940c0354b..afed153f928 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -61,7 +61,7 @@ SET(LMMS_SRCS gui/instrument/EnvelopeAndLfoView.cpp gui/instrument/InstrumentFunctionViews.cpp gui/instrument/InstrumentMidiIOView.cpp - gui/instrument/InstrumentMiscView.cpp + gui/instrument/InstrumentTuningView.cpp gui/instrument/InstrumentSoundShapingView.cpp gui/instrument/InstrumentTrackWindow.cpp gui/instrument/InstrumentView.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index c0763d542ae..181e67cd749 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -23,7 +23,6 @@ * */ - #include #include #include @@ -126,7 +125,7 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_filterEdit->setPlaceholderText( tr("Search") ); m_filterEdit->setClearButtonEnabled( true ); connect( m_filterEdit, SIGNAL( textEdited( const QString& ) ), - this, SLOT( filterItems( const QString& ) ) ); + this, SLOT( filterAndExpandItems( const QString& ) ) ); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); @@ -145,53 +144,95 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, auto filterFocusShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this, SLOT(giveFocusToFilter())); filterFocusShortcut->setContext(Qt::WidgetWithChildrenShortcut); + m_previousFilterValue = ""; + reloadTree(); show(); } -bool FileBrowser::filterItems( const QString & filter, QTreeWidgetItem * item ) +void FileBrowser::saveDirectoriesStates() +{ + m_savedExpandedDirs = m_fileBrowserTreeWidget->expandedDirs(); +} + +void FileBrowser::restoreDirectoriesStates() +{ + expandItems(nullptr, m_savedExpandedDirs); +} + +bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item) { - // call with item=NULL to filter the entire tree + // Call with item = nullptr to filter the entire tree + + if (item == nullptr) + { + // First search character so need to save current expanded directories + if (m_previousFilterValue.isEmpty()) + { + saveDirectoriesStates(); + } + + m_previousFilterValue = filter; + } + + if (filter.isEmpty()) + { + // Restore previous expanded directories + if (item == nullptr) + { + restoreDirectoriesStates(); + } + + return false; + } + bool anyMatched = false; int numChildren = item ? item->childCount() : m_fileBrowserTreeWidget->topLevelItemCount(); - for( int i = 0; i < numChildren; ++i ) + + for (int i = 0; i < numChildren; ++i) { QTreeWidgetItem * it = item ? item->child( i ) : m_fileBrowserTreeWidget->topLevelItem(i); - // is directory? - if( it->childCount() ) + auto d = dynamic_cast(it); + if (d) { - // matches filter? - if( it->text( 0 ). - contains( filter, Qt::CaseInsensitive ) ) + if (it->text(0).contains(filter, Qt::CaseInsensitive)) { - // yes, then show everything below - it->setHidden( false ); - filterItems( QString(), it ); + it->setHidden(false); + it->setExpanded(true); + filterAndExpandItems(QString(), it); anyMatched = true; } else { - // only show if item below matches filter - bool didMatch = filterItems( filter, it ); - it->setHidden( !didMatch ); + // Expanding is required when recursive to load in its contents, even if it's collapsed right afterward + it->setExpanded(true); + + bool didMatch = filterAndExpandItems(filter, it); + it->setHidden(!didMatch); + it->setExpanded(didMatch); anyMatched = anyMatched || didMatch; } } - // a standard item (i.e. no file or directory item?) - else if( it->type() == QTreeWidgetItem::Type ) - { - // hide if there's any filter - it->setHidden( !filter.isEmpty() ); - } + else { - // file matches filter? - bool didMatch = it->text( 0 ). - contains( filter, Qt::CaseInsensitive ); - it->setHidden( !didMatch ); - anyMatched = anyMatched || didMatch; + auto f = dynamic_cast(it); + if (f) + { + // File + bool didMatch = it->text(0).contains(filter, Qt::CaseInsensitive); + it->setHidden(!didMatch); + anyMatched = anyMatched || didMatch; + } + + // A standard item (i.e. no file or directory item?) + else + { + // Hide if there's any filter + it->setHidden(!filter.isEmpty()); + } } } @@ -201,15 +242,20 @@ bool FileBrowser::filterItems( const QString & filter, QTreeWidgetItem * item ) void FileBrowser::reloadTree() { - QList expandedDirs = m_fileBrowserTreeWidget->expandedDirs(); - const QString text = m_filterEdit->text(); - m_filterEdit->clear(); + if (m_filterEdit->text().isEmpty()) + { + saveDirectoriesStates(); + } + m_fileBrowserTreeWidget->clear(); + QStringList paths = m_directories.split('*'); + if (m_showUserContent && !m_showUserContent->isChecked()) { paths.removeAll(m_userDir); } + if (m_showFactoryContent && !m_showFactoryContent->isChecked()) { paths.removeAll(m_factoryDir); @@ -222,9 +268,15 @@ void FileBrowser::reloadTree() addItems(path); } } - expandItems(nullptr, expandedDirs); - m_filterEdit->setText( text ); - filterItems( text ); + + if (m_filterEdit->text().isEmpty()) + { + restoreDirectoriesStates(); + } + else + { + filterAndExpandItems(m_filterEdit->text()); + } } @@ -240,12 +292,16 @@ void FileBrowser::expandItems(QTreeWidgetItem* item, QList expandedDirs { // Expanding is required when recursive to load in its contents, even if it's collapsed right afterward if (m_recurse) { d->setExpanded(true); } + d->setExpanded(expandedDirs.contains(d->fullName())); + if (m_recurse && it->childCount()) { expandItems(it, expandedDirs); } } + + it->setHidden(false); } } @@ -416,8 +472,6 @@ QList FileBrowserTreeWidget::expandedDirs( QTreeWidgetItem * item ) con } - - void FileBrowserTreeWidget::keyPressEvent(QKeyEvent * ke ) { // Shorter names for some commonly used properties of the event diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 3fd1d44b109..830a994c8c6 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -39,6 +39,7 @@ #include "GuiApplication.h" #include "embed.h" #include "gui_templates.h" +#include "lmms_math.h" #include "Lv2ControlBase.h" #include "Lv2Manager.h" #include "Lv2Proc.h" @@ -51,13 +52,13 @@ namespace lmms::gui { -Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : - LinkedModelGroupView (parent, ctrlBase, colNum) +Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* proc, int colNum) : + LinkedModelGroupView (parent, proc, colNum) { - class SetupWidget : public Lv2Ports::ConstVisitor + class SetupTheWidget : public Lv2Ports::ConstVisitor { public: - QWidget* m_par; // input + QWidget* m_parent; // input const LilvNode* m_commentUri; // input Control* m_control = nullptr; // output void visit(const Lv2Ports::Control& port) override @@ -69,20 +70,22 @@ Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : switch (port.m_vis) { case PortVis::Generic: - m_control = new KnobControl(m_par); + m_control = new KnobControl(m_parent); break; case PortVis::Integer: { sample_rate_t sr = Engine::audioEngine()->processingSampleRate(); - m_control = new LcdControl((port.max(sr) <= 9.0f) ? 1 : 2, - m_par); + auto pMin = port.min(sr); + auto pMax = port.max(sr); + int numDigits = std::max(numDigitsAsInt(pMin), numDigitsAsInt(pMax)); + m_control = new LcdControl(numDigits, m_parent); break; } case PortVis::Enumeration: - m_control = new ComboControl(m_par); + m_control = new ComboControl(m_parent); break; case PortVis::Toggled: - m_control = new CheckControl(m_par); + m_control = new CheckControl(m_parent); break; } m_control->setText(port.name()); @@ -100,14 +103,14 @@ Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : }; AutoLilvNode commentUri = uri(LILV_NS_RDFS "comment"); - ctrlBase->foreach_port( + proc->foreach_port( [this, &commentUri](const Lv2Ports::PortBase* port) { if(!lilv_port_has_property(port->m_plugin, port->m_port, uri(LV2_PORT_PROPS__notOnGUI).get())) { - SetupWidget setup; - setup.m_par = this; + SetupTheWidget setup; + setup.m_parent = this; setup.m_commentUri = commentUri.get(); port->accept(setup); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 559756169c6..10805fe01c4 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -363,10 +363,12 @@ void MainWindow::finalize() } edit_menu->addSeparator(); - edit_menu->addAction( embed::getIconPixmap( "setup_general" ), - tr( "Settings" ), - this, SLOT(showSettingsDialog())); - connect( edit_menu, SIGNAL(aboutToShow()), this, SLOT(updateUndoRedoButtons())); + edit_menu->addAction(embed::getIconPixmap("microtuner"), tr("Scales and keymaps"), + this, SLOT(toggleMicrotunerWin())); + edit_menu->addAction(embed::getIconPixmap("setup_general"), tr("Settings"), + this, SLOT(showSettingsDialog())); + + connect(edit_menu, SIGNAL(aboutToShow()), this, SLOT(updateUndoRedoButtons())); m_viewMenu = new QMenu( this ); menuBar()->addMenu( m_viewMenu )->setText( tr( "&View" ) ); @@ -485,10 +487,6 @@ void MainWindow::finalize() tr("Show/hide project notes") + " (Ctrl+7)", this, SLOT(toggleProjectNotesWin()), m_toolBar); project_notes_window->setShortcut( Qt::CTRL + Qt::Key_7 ); - auto microtuner_window = new ToolButton(embed::getIconPixmap("microtuner"), - tr("Microtuner configuration") + " (Ctrl+8)", this, SLOT(toggleMicrotunerWin()), m_toolBar); - microtuner_window->setShortcut( Qt::CTRL + Qt::Key_8 ); - m_toolBarLayout->addWidget( song_editor_window, 1, 1 ); m_toolBarLayout->addWidget( pattern_editor_window, 1, 2 ); m_toolBarLayout->addWidget( piano_roll_window, 1, 3 ); @@ -496,7 +494,6 @@ void MainWindow::finalize() m_toolBarLayout->addWidget( mixer_window, 1, 5 ); m_toolBarLayout->addWidget( controllers_window, 1, 6 ); m_toolBarLayout->addWidget( project_notes_window, 1, 7 ); - m_toolBarLayout->addWidget( microtuner_window, 1, 8 ); m_toolBarLayout->setColumnStretch( 100, 1 ); // setup-dialog opened before? @@ -1100,10 +1097,6 @@ void MainWindow::updateViewMenu() tr( "Project Notes" ) + "\tCtrl+7", this, SLOT(toggleProjectNotesWin()) ); - m_viewMenu->addAction(embed::getIconPixmap( "microtuner" ), - tr( "Microtuner" ) + "\tCtrl+8", - this, SLOT(toggleMicrotunerWin()) - ); m_viewMenu->addSeparator(); diff --git a/src/gui/MicrotunerConfig.cpp b/src/gui/MicrotunerConfig.cpp index 7ab4cc0b1ac..4156b9e79df 100644 --- a/src/gui/MicrotunerConfig.cpp +++ b/src/gui/MicrotunerConfig.cpp @@ -56,8 +56,8 @@ namespace lmms::gui MicrotunerConfig::MicrotunerConfig() : QWidget(), - m_scaleComboModel(nullptr, tr("Selected scale")), - m_keymapComboModel(nullptr, tr("Selected keymap")), + m_scaleComboModel(nullptr, tr("Selected scale slot")), + m_keymapComboModel(nullptr, tr("Selected keymap slot")), m_firstKeyModel(0, 0, NumKeys - 1, nullptr, tr("First key")), m_lastKeyModel(NumKeys - 1, 0, NumKeys - 1, nullptr, tr("Last key")), m_middleKeyModel(DefaultMiddleKey, 0, NumKeys - 1, nullptr, tr("Middle key")), @@ -75,7 +75,7 @@ MicrotunerConfig::MicrotunerConfig() : #endif setWindowIcon(embed::getIconPixmap("microtuner")); - setWindowTitle(tr("Microtuner")); + setWindowTitle(tr("Microtuner Configuration")); // Organize into 2 main columns: scales and keymaps auto microtunerLayout = new QGridLayout(); @@ -84,7 +84,7 @@ MicrotunerConfig::MicrotunerConfig() : // ---------------------------------- // Scale sub-column // - auto scaleLabel = new QLabel(tr("Scale:")); + auto scaleLabel = new QLabel(tr("Scale slot to edit:")); microtunerLayout->addWidget(scaleLabel, 0, 0, 1, 2, Qt::AlignBottom); for (unsigned int i = 0; i < MaxScaleCount; i++) @@ -102,6 +102,8 @@ MicrotunerConfig::MicrotunerConfig() : auto loadScaleButton = new QPushButton(tr("Load")); auto saveScaleButton = new QPushButton(tr("Save")); + loadScaleButton->setToolTip(tr("Load scale definition from a file.")); + saveScaleButton->setToolTip(tr("Save scale definition to a file.")); microtunerLayout->addWidget(loadScaleButton, 3, 0, 1, 1); microtunerLayout->addWidget(saveScaleButton, 3, 1, 1, 1); connect(loadScaleButton, &QPushButton::clicked, [=] {loadScaleFromFile();}); @@ -112,14 +114,15 @@ MicrotunerConfig::MicrotunerConfig() : m_scaleTextEdit->setToolTip(tr("Enter intervals on separate lines. Numbers containing a decimal point are treated as cents.\nOther inputs are treated as integer ratios and must be in the form of \'a/b\' or \'a\'.\nUnity (0.0 cents or ratio 1/1) is always present as a hidden first value; do not enter it manually.")); microtunerLayout->addWidget(m_scaleTextEdit, 4, 0, 2, 2); - auto applyScaleButton = new QPushButton(tr("Apply scale")); + auto applyScaleButton = new QPushButton(tr("Apply scale changes")); + applyScaleButton->setToolTip(tr("Verify and apply changes made to the selected scale. To use the scale, select it in the settings of a supported instrument.")); microtunerLayout->addWidget(applyScaleButton, 6, 0, 1, 2); connect(applyScaleButton, &QPushButton::clicked, [=] {applyScale();}); // ---------------------------------- // Mapping sub-column // - auto keymapLabel = new QLabel(tr("Keymap:")); + auto keymapLabel = new QLabel(tr("Keymap slot to edit:")); microtunerLayout->addWidget(keymapLabel, 0, 2, 1, 2, Qt::AlignBottom); for (unsigned int i = 0; i < MaxKeymapCount; i++) @@ -137,6 +140,8 @@ MicrotunerConfig::MicrotunerConfig() : auto loadKeymapButton = new QPushButton(tr("Load")); auto saveKeymapButton = new QPushButton(tr("Save")); + loadKeymapButton->setToolTip(tr("Load key mapping definition from a file.")); + saveKeymapButton->setToolTip(tr("Save key mapping definition to a file.")); microtunerLayout->addWidget(loadKeymapButton, 3, 2, 1, 1); microtunerLayout->addWidget(saveKeymapButton, 3, 3, 1, 1); connect(loadKeymapButton, &QPushButton::clicked, [=] {loadKeymapFromFile();}); @@ -181,7 +186,8 @@ MicrotunerConfig::MicrotunerConfig() : baseFreqSpin->setToolTip(tr("Base note frequency")); keymapRangeLayout->addWidget(baseFreqSpin, 1, 1, 1, 2); - auto applyKeymapButton = new QPushButton(tr("Apply keymap")); + auto applyKeymapButton = new QPushButton(tr("Apply keymap changes")); + applyKeymapButton->setToolTip(tr("Verify and apply changes made to the selected key mapping. To use the mapping, select it in the settings of a supported instrument.")); microtunerLayout->addWidget(applyKeymapButton, 6, 2, 1, 2); connect(applyKeymapButton, &QPushButton::clicked, [=] {applyKeymap();}); diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 0edebcb8a94..dff19ca3eb7 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -248,8 +248,8 @@ void MixerView::refreshDisplay() // update the and max. channel number for every instrument void MixerView::updateMaxChannelSelector() { - TrackContainer::TrackList songTracks = Engine::getSong()->tracks(); - TrackContainer::TrackList patternStoreTracks = Engine::patternStore()->tracks(); + const TrackContainer::TrackList& songTracks = Engine::getSong()->tracks(); + const TrackContainer::TrackList& patternStoreTracks = Engine::patternStore()->tracks(); for (const auto& trackList : {songTracks, patternStoreTracks}) { diff --git a/src/gui/SampleTrackWindow.cpp b/src/gui/SampleTrackWindow.cpp index 68b5eb8a223..f6d7f9ea1d9 100644 --- a/src/gui/SampleTrackWindow.cpp +++ b/src/gui/SampleTrackWindow.cpp @@ -84,7 +84,6 @@ SampleTrackWindow::SampleTrackWindow(SampleTrackView * tv) : // setup line edit for changing sample track name m_nameLineEdit = new QLineEdit; - m_nameLineEdit->setFont(pointSize<9>(m_nameLineEdit->font())); connect(m_nameLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(textChanged(const QString&))); diff --git a/src/gui/SideBarWidget.cpp b/src/gui/SideBarWidget.cpp index 60760ba5904..c218bedd335 100644 --- a/src/gui/SideBarWidget.cpp +++ b/src/gui/SideBarWidget.cpp @@ -62,16 +62,16 @@ void SideBarWidget::paintEvent( QPaintEvent * ) QFont f = p.font(); f.setBold( true ); - f.setUnderline( true ); + f.setUnderline(false); f.setPointSize( f.pointSize() + 2 ); p.setFont( f ); p.setPen( palette().highlightedText().color() ); - const int tx = m_icon.width()+4; + const int tx = m_icon.width() + 8; QFontMetrics metrics( f ); - const int ty = metrics.ascent(); + const int ty = (metrics.ascent() + m_icon.height()) / 2; p.drawText( tx, ty, m_title ); p.drawPixmap( 2, 2, m_icon.transformed( QTransform().rotate( -90 ) ) ); diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index 7a7a19c1119..de7690d26e1 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -25,6 +25,7 @@ #include "ClipView.h" #include +#include #include #include @@ -545,6 +546,7 @@ DataFile ClipView::createClipDataFiles( // Insert into the dom under the "clips" element Track* clipTrack = clipView->m_trackView->getTrack(); int trackIndex = std::distance(tc->tracks().begin(), std::find(tc->tracks().begin(), tc->tracks().end(), clipTrack)); + assert(trackIndex != tc->tracks().size()); QDomElement clipElement = dataFile.createElement("clip"); clipElement.setAttribute( "trackIndex", trackIndex ); clipElement.setAttribute( "trackType", static_cast(clipTrack->type()) ); @@ -1308,7 +1310,7 @@ void ClipView::mergeClips(QVector clipvs) continue; } - NoteVector currentClipNotes = mcView->getMidiClip()->notes(); + const NoteVector& currentClipNotes = mcView->getMidiClip()->notes(); TimePos mcViewPos = mcView->getMidiClip()->startPosition(); for (Note* note: currentClipNotes) diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 79c4cd73d68..151df8d3c3c 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -523,7 +523,8 @@ void MidiClipView::paintEvent( QPaintEvent * ) p.scale(width(), height() - distanceToTop - 2 * notesBorder); // set colour based on mute status - QColor noteFillColor = muted ? getMutedNoteFillColor() : getNoteFillColor(); + QColor noteFillColor = muted ? getMutedNoteFillColor().lighter(200) + : (c.lightness() > 175 ? getNoteFillColor().darker(400) : getNoteFillColor()); QColor noteBorderColor = muted ? getMutedNoteBorderColor() : ( m_clip->hasColor() ? c.lighter( 200 ) : getNoteBorderColor() ); diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index a24165332d0..f98066bbaa2 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -531,7 +531,7 @@ void AutomationEditor::mousePressEvent( QMouseEvent* mouseEvent ) : POS(clickedNode) ), level, - true, + clickedNode == tm.end(), mouseEvent->modifiers() & Qt::ControlModifier ); diff --git a/src/gui/editors/PatternEditor.cpp b/src/gui/editors/PatternEditor.cpp index 229c90bc234..5237690a7e7 100644 --- a/src/gui/editors/PatternEditor.cpp +++ b/src/gui/editors/PatternEditor.cpp @@ -69,7 +69,7 @@ void PatternEditor::cloneSteps() void PatternEditor::removeSteps() { - TrackContainer::TrackList tl = model()->tracks(); + const TrackContainer::TrackList& tl = model()->tracks(); for (const auto& track : tl) { @@ -176,7 +176,7 @@ void PatternEditor::updatePosition() void PatternEditor::makeSteps( bool clone ) { - TrackContainer::TrackList tl = model()->tracks(); + const TrackContainer::TrackList& tl = model()->tracks(); for (const auto& track : tl) { diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 0cc8257255f..cef2205d266 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -130,7 +130,10 @@ QPixmap* PianoRoll::s_toolKnife = nullptr; SimpleTextFloat * PianoRoll::s_textFloat = nullptr; -static std::array s_noteStrings {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; +static std::array s_noteStrings { + "C", "C\u266F / D\u266D", "D", "D\u266F / E\u266D", "E", "F", "F\u266F / G\u266D", + "G", "G\u266F / A\u266D", "A", "A\u266F / B\u266D", "B" +}; static QString getNoteString(int key) { @@ -741,10 +744,10 @@ void PianoRoll::fitNoteLengths(bool fill) { if (!hasValidMidiClip()) { return; } m_midiClip->addJournalCheckPoint(); + m_midiClip->rearrangeAllNotes(); // Reference notes - NoteVector refNotes = m_midiClip->notes(); - std::sort(refNotes.begin(), refNotes.end(), Note::lessThan); + const NoteVector& refNotes = m_midiClip->notes(); // Notes to edit NoteVector notes = getSelectedNotes(); @@ -762,7 +765,7 @@ void PianoRoll::fitNoteLengths(bool fill) } int length; - NoteVector::iterator ref = refNotes.begin(); + auto ref = refNotes.begin(); for (Note* note : notes) { // Fast forward to next reference note @@ -797,14 +800,11 @@ void PianoRoll::constrainNoteLengths(bool constrainMax) if (!hasValidMidiClip()) { return; } m_midiClip->addJournalCheckPoint(); - NoteVector notes = getSelectedNotes(); - if (notes.empty()) - { - notes = m_midiClip->notes(); - } + const NoteVector selectedNotes = getSelectedNotes(); + const auto& notes = selectedNotes.empty() ? m_midiClip->notes() : selectedNotes; - TimePos bound = m_lenOfNewNotes; // will be length of last note - for (Note* note : notes) + TimePos bound = m_lenOfNewNotes; // will be length of last note + for (auto note : notes) { if (constrainMax ? note->length() > bound : note->length() < bound) { @@ -1207,11 +1207,11 @@ void PianoRoll::shiftSemiTone(int amount) //Shift notes by amount semitones auto selectedNotes = getSelectedNotes(); //If no notes are selected, shift all of them, otherwise shift selection - if (selectedNotes.empty()) { return shiftSemiTone(m_midiClip->notes(), amount); } - else { return shiftSemiTone(selectedNotes, amount); } + if (selectedNotes.empty()) { shiftSemiTone(m_midiClip->notes(), amount); } + else { shiftSemiTone(selectedNotes, amount); } } -void PianoRoll::shiftSemiTone(NoteVector notes, int amount) +void PianoRoll::shiftSemiTone(const NoteVector& notes, int amount) { m_midiClip->addJournalCheckPoint(); for (Note *note : notes) { note->setKey( note->key() + amount ); } @@ -1232,11 +1232,11 @@ void PianoRoll::shiftPos(int amount) //Shift notes pos by amount auto selectedNotes = getSelectedNotes(); //If no notes are selected, shift all of them, otherwise shift selection - if (selectedNotes.empty()) { return shiftPos(m_midiClip->notes(), amount); } - else { return shiftPos(selectedNotes, amount); } + if (selectedNotes.empty()) { shiftPos(m_midiClip->notes(), amount); } + else { shiftPos(selectedNotes, amount); } } -void PianoRoll::shiftPos(NoteVector notes, int amount) +void PianoRoll::shiftPos(const NoteVector& notes, int amount) { m_midiClip->addJournalCheckPoint(); @@ -1722,10 +1722,10 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) const NoteVector & notes = m_midiClip->notes(); // will be our iterator in the following loop - auto it = notes.begin() + notes.size() - 1; + auto it = notes.rbegin(); // loop through whole note-vector... - for( int i = 0; i < notes.size(); ++i ) + while (it != notes.rend()) { Note *note = *it; TimePos len = note->length(); @@ -1750,7 +1750,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) { break; } - --it; + ++it; } // first check whether the user clicked in note-edit- @@ -1772,7 +1772,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) Note * created_new_note = nullptr; // did it reach end of vector because // there's no note?? - if( it == notes.begin()-1 ) + if (it == notes.rend()) { is_new_note = true; m_midiClip->addJournalCheckPoint(); @@ -1819,8 +1819,8 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) // reset it so that it can be used for // ops (move, resize) after this // code-block - it = notes.begin(); - while( it != notes.end() && *it != created_new_note ) + it = notes.rbegin(); + while (it != notes.rend() && *it != created_new_note) { ++it; } @@ -1936,7 +1936,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) { // erase single note m_mouseDownRight = true; - if( it != notes.begin()-1 ) + if (it != notes.rend()) { m_midiClip->addJournalCheckPoint(); m_midiClip->removeNote( *it ); @@ -2516,7 +2516,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) bool altPressed = me->modifiers() & Qt::AltModifier; // We iterate from last note in MIDI clip to the first, // chronologically - auto it = notes.begin() + notes.size() - 1; + auto it = notes.rbegin(); for( int i = 0; i < notes.size(); ++i ) { Note* n = *it; @@ -2559,7 +2559,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) } - --it; + ++it; } // Emit MIDI clip has changed @@ -2578,10 +2578,10 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) const NoteVector & notes = m_midiClip->notes(); // will be our iterator in the following loop - auto it = notes.begin() + notes.size() - 1; + auto it = notes.rbegin(); // loop through whole note-vector... - for( int i = 0; i < notes.size(); ++i ) + while (it != notes.rend()) { Note *note = *it; // and check whether the cursor is over an @@ -2594,12 +2594,12 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) { break; } - --it; + ++it; } // did it reach end of vector because there's // no note?? - if( it != notes.begin()-1 ) + if (it != notes.rend()) { Note *note = *it; // x coordinate of the right edge of the note @@ -4134,9 +4134,9 @@ void PianoRoll::finishRecordNote(const Note & n ) { if( it->key() == n.key() ) { - Note n1( n.length(), it->pos(), + Note n1(n.length(), it->pos(), it->key(), it->getVolume(), - it->getPanning() ); + it->getPanning(), n.detuning()); n1.quantizeLength( quantization() ); m_midiClip->addNote( n1 ); update(); diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index 43cca0dac1f..28cd8c6c8aa 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -49,7 +49,7 @@ #include "InstrumentFunctions.h" #include "InstrumentFunctionViews.h" #include "InstrumentMidiIOView.h" -#include "InstrumentMiscView.h" +#include "InstrumentTuningView.h" #include "InstrumentSoundShapingView.h" #include "InstrumentTrack.h" #include "InstrumentTrackView.h" @@ -255,25 +255,25 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : instrumentFunctionsLayout->addStretch(); // MIDI tab - m_midiView = new InstrumentMidiIOView( m_tabWidget ); + m_midiView = new InstrumentMidiIOView(m_tabWidget); // FX tab - m_effectView = new EffectRackView( m_track->m_audioPort.effects(), m_tabWidget ); + m_effectView = new EffectRackView(m_track->m_audioPort.effects(), m_tabWidget); - // MISC tab - m_miscView = new InstrumentMiscView( m_track, m_tabWidget ); + // Tuning tab + m_tuningView = new InstrumentTuningView(m_track, m_tabWidget); - m_tabWidget->addTab( m_ssView, tr( "Envelope, filter & LFO" ), "env_lfo_tab", 1 ); - m_tabWidget->addTab( instrumentFunctions, tr( "Chord stacking & arpeggio" ), "func_tab", 2 ); - m_tabWidget->addTab( m_effectView, tr( "Effects" ), "fx_tab", 3 ); - m_tabWidget->addTab( m_midiView, tr( "MIDI" ), "midi_tab", 4 ); - m_tabWidget->addTab( m_miscView, tr( "Miscellaneous" ), "misc_tab", 5 ); + m_tabWidget->addTab(m_ssView, tr("Envelope, filter & LFO"), "env_lfo_tab", 1); + m_tabWidget->addTab(instrumentFunctions, tr("Chord stacking & arpeggio"), "func_tab", 2); + m_tabWidget->addTab(m_effectView, tr("Effects"), "fx_tab", 3); + m_tabWidget->addTab(m_midiView, tr("MIDI"), "midi_tab", 4); + m_tabWidget->addTab(m_tuningView, tr("Tuning and transposition"), "tuning_tab", 5); adjustTabSize(m_ssView); adjustTabSize(instrumentFunctions); m_effectView->resize(EffectRackView::DEFAULT_WIDTH, INSTRUMENT_HEIGHT - 4 - 1); adjustTabSize(m_midiView); - adjustTabSize(m_miscView); + adjustTabSize(m_tuningView); // setup piano-widget m_pianoView = new PianoView( this ); @@ -376,12 +376,14 @@ void InstrumentTrackWindow::modelChanged() if (m_track->instrument() && m_track->instrument()->flags().testFlag(Instrument::Flag::IsMidiBased)) { - m_miscView->microtunerGroupBox()->hide(); + m_tuningView->microtunerNotSupportedLabel()->show(); + m_tuningView->microtunerGroupBox()->hide(); m_track->m_microtuner.enabledModel()->setValue(false); } else { - m_miscView->microtunerGroupBox()->show(); + m_tuningView->microtunerNotSupportedLabel()->hide(); + m_tuningView->microtunerGroupBox()->show(); } m_ssView->setModel( &m_track->m_soundShaping ); @@ -389,11 +391,11 @@ void InstrumentTrackWindow::modelChanged() m_arpeggioView->setModel( &m_track->m_arpeggio ); m_midiView->setModel( &m_track->m_midiPort ); m_effectView->setModel( m_track->m_audioPort.effects() ); - m_miscView->pitchGroupBox()->setModel(&m_track->m_useMasterPitchModel); - m_miscView->microtunerGroupBox()->setModel(m_track->m_microtuner.enabledModel()); - m_miscView->scaleCombo()->setModel(m_track->m_microtuner.scaleModel()); - m_miscView->keymapCombo()->setModel(m_track->m_microtuner.keymapModel()); - m_miscView->rangeImportCheckbox()->setModel(m_track->m_microtuner.keyRangeImportModel()); + m_tuningView->pitchGroupBox()->setModel(&m_track->m_useMasterPitchModel); + m_tuningView->microtunerGroupBox()->setModel(m_track->m_microtuner.enabledModel()); + m_tuningView->scaleCombo()->setModel(m_track->m_microtuner.scaleModel()); + m_tuningView->keymapCombo()->setModel(m_track->m_microtuner.keymapModel()); + m_tuningView->rangeImportCheckbox()->setModel(m_track->m_microtuner.keyRangeImportModel()); updateName(); } diff --git a/src/gui/instrument/InstrumentMiscView.cpp b/src/gui/instrument/InstrumentTuningView.cpp similarity index 69% rename from src/gui/instrument/InstrumentMiscView.cpp rename to src/gui/instrument/InstrumentTuningView.cpp index 514db579cb6..355d7d18c73 100644 --- a/src/gui/instrument/InstrumentMiscView.cpp +++ b/src/gui/instrument/InstrumentTuningView.cpp @@ -1,8 +1,8 @@ /* - * InstrumentMiscView.cpp - Miscellaneous instrument settings + * InstrumentTuningView.cpp - Instrument settings for tuning and transpositions * * Copyright (c) 2005-2014 Tobias Doerffel - * Copyright (c) 2020 Martin Pavelek + * Copyright (c) 2020-2022 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -23,24 +23,28 @@ * */ -#include "InstrumentMiscView.h" +#include "InstrumentTuningView.h" #include #include +#include #include #include "ComboBox.h" #include "GroupBox.h" +#include "GuiApplication.h" #include "gui_templates.h" #include "InstrumentTrack.h" #include "LedCheckBox.h" +#include "MainWindow.h" +#include "PixmapButton.h" namespace lmms::gui { -InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) : +InstrumentTuningView::InstrumentTuningView(InstrumentTrack *it, QWidget *parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); @@ -60,6 +64,11 @@ InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) : masterPitchLayout->addWidget(tlabel); // Microtuner settings + m_microtunerNotSupportedLabel = new QLabel(tr("Microtuner is not available for MIDI-based instruments.")); + m_microtunerNotSupportedLabel->setWordWrap(true); + m_microtunerNotSupportedLabel->hide(); + layout->addWidget(m_microtunerNotSupportedLabel); + m_microtunerGroupBox = new GroupBox(tr("MICROTUNER")); m_microtunerGroupBox->setModel(it->m_microtuner.enabledModel()); layout->addWidget(m_microtunerGroupBox); @@ -67,8 +76,22 @@ InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) : auto microtunerLayout = new QVBoxLayout(m_microtunerGroupBox); microtunerLayout->setContentsMargins(8, 18, 8, 8); + auto scaleEditLayout = new QHBoxLayout(); + scaleEditLayout->setContentsMargins(0, 0, 4, 0); + microtunerLayout->addLayout(scaleEditLayout); + auto scaleLabel = new QLabel(tr("Active scale:")); - microtunerLayout->addWidget(scaleLabel); + scaleEditLayout->addWidget(scaleLabel); + + QPixmap editPixmap(embed::getIconPixmap("edit_draw_small")); + auto editPixButton = new PixmapButton(this, tr("Edit scales and keymaps")); + editPixButton->setToolTip(tr("Edit scales and keymaps")); + editPixButton->setInactiveGraphic(editPixmap); + editPixButton->setActiveGraphic(editPixmap); + editPixButton->setFixedSize(16, 16); + connect(editPixButton, SIGNAL(clicked()), getGUI()->mainWindow(), SLOT(toggleMicrotunerWin())); + + scaleEditLayout->addWidget(editPixButton); m_scaleCombo = new ComboBox(); m_scaleCombo->setModel(it->m_microtuner.scaleModel()); diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index 20db2e8e83f..d20cbcac52e 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -322,70 +322,65 @@ void PianoView::modelChanged() -// gets the key from the given mouse-position +// Gets the key from the given mouse position /*! \brief Get the key from the mouse position in the piano display * - * First we determine it roughly by the position of the point given in - * white key widths from our start. We then add in any black keys that - * might have been skipped over (they take a key number, but no 'white - * key' space). We then add in our starting key number. - * - * We then determine whether it was a black key that was pressed by - * checking whether it was within the vertical range of black keys. - * Black keys sit exactly between white keys on this keyboard, so - * we then shift the note down or up if we were in the left or right - * half of the white note. We only do this, of course, if the white - * note has a black key on that side, so to speak. - * - * This function returns const because there is a linear mapping from - * the point given to the key returned that never changes. - * - * \param _p The point that the mouse was pressed. + * \param p The point that the mouse was pressed. */ -int PianoView::getKeyFromMouse( const QPoint & _p ) const +int PianoView::getKeyFromMouse(const QPoint& p) const { - int offset = _p.x() % PW_WHITE_KEY_WIDTH; - if( offset < 0 ) offset += PW_WHITE_KEY_WIDTH; - int key_num = ( _p.x() - offset) / PW_WHITE_KEY_WIDTH; + // The left-most key visible in the piano display is always white + const int startingWhiteKey = m_pianoScroll->value(); - for( int i = 0; i <= key_num; ++i ) - { - if ( Piano::isBlackKey( m_startKey+i ) ) - { - ++key_num; - } - } - for( int i = 0; i >= key_num; --i ) - { - if ( Piano::isBlackKey( m_startKey+i ) ) - { - --key_num; - } - } + // Adjust the mouse x position as if x == 0 was the left side of the lowest key + const int adjX = p.x() + (startingWhiteKey * PW_WHITE_KEY_WIDTH); + + // Can early return for notes too low + if (adjX <= 0) { return 0; } + + // Now we can calculate the key number (in only white keys) and the octave + const int whiteKey = adjX / PW_WHITE_KEY_WIDTH; + const int octave = whiteKey / Piano::WhiteKeysPerOctave; - key_num += m_startKey; + // Calculate for full octaves + int key = octave * KeysPerOctave; - // is it a black key? - if( _p.y() < PIANO_BASE + PW_BLACK_KEY_HEIGHT ) + // Adjust for white notes in the current octave + // (WhiteKeys maps each white key to the number of notes to their left in the octave) + key += static_cast(WhiteKeys[whiteKey % Piano::WhiteKeysPerOctave]); + + // Might be a black key, which would require further adjustment + if (p.y() < PIANO_BASE + PW_BLACK_KEY_HEIGHT) { - // then do extra checking whether the mouse-cursor is over - // a black key - if( key_num > 0 && Piano::isBlackKey( key_num-1 ) && - offset <= ( PW_WHITE_KEY_WIDTH / 2 ) - - ( PW_BLACK_KEY_WIDTH / 2 ) ) + // Maps white keys to neighboring black keys + static constexpr std::array neighboringKeyMap { + std::pair{ 0, 1 }, // C --> no B#; C# + std::pair{ 1, 1 }, // D --> C#; D# + std::pair{ 1, 0 }, // E --> D#; no E# + std::pair{ 0, 1 }, // F --> no E#; F# + std::pair{ 1, 1 }, // G --> F#; G# + std::pair{ 1, 1 }, // A --> G#; A# + std::pair{ 1, 0 }, // B --> A#; no B# + }; + + const auto neighboringBlackKeys = neighboringKeyMap[whiteKey % Piano::WhiteKeysPerOctave]; + const int offset = adjX - (whiteKey * PW_WHITE_KEY_WIDTH); // mouse X offset from white key + + if (offset < PW_BLACK_KEY_WIDTH / 2) { - --key_num; + // At the location of a (possibly non-existent) black key on the left side + key -= neighboringBlackKeys.first; } - if( key_num < NumKeys - 1 && Piano::isBlackKey( key_num+1 ) && - offset >= ( PW_WHITE_KEY_WIDTH - - PW_BLACK_KEY_WIDTH / 2 ) ) + else if (offset > PW_WHITE_KEY_WIDTH - (PW_BLACK_KEY_WIDTH / 2)) { - ++key_num; + // At the location of a (possibly non-existent) black key on the right side + key += neighboringBlackKeys.second; } + + // For white keys in between black keys, no further adjustment is needed } - // some range-checking-stuff - return qBound( 0, key_num, NumKeys - 1 ); + return std::clamp(key, 0, NumKeys - 1); } @@ -396,12 +391,12 @@ int PianoView::getKeyFromMouse( const QPoint & _p ) const * * We need to update our start key position based on the new position. * - * \param _new_pos the new key position. + * \param newPos the new key position, counting only white keys. */ -void PianoView::pianoScrolled(int new_pos) +void PianoView::pianoScrolled(int newPos) { - m_startKey = static_cast(new_pos / Piano::WhiteKeysPerOctave) - + WhiteKeys[new_pos % Piano::WhiteKeysPerOctave]; + m_startKey = static_cast(newPos / Piano::WhiteKeysPerOctave) + + WhiteKeys[newPos % Piano::WhiteKeysPerOctave]; update(); } diff --git a/src/gui/menus/MidiPortMenu.cpp b/src/gui/menus/MidiPortMenu.cpp index 296be3506f1..b99c3a0b72f 100644 --- a/src/gui/menus/MidiPortMenu.cpp +++ b/src/gui/menus/MidiPortMenu.cpp @@ -34,7 +34,6 @@ MidiPortMenu::MidiPortMenu( MidiPort::Mode _mode ) : ModelView( nullptr, this ), m_mode( _mode ) { - setFont( pointSize<9>( font() ) ); connect( this, SIGNAL(triggered(QAction*)), this, SLOT(activatedPort(QAction*))); } diff --git a/src/gui/modals/ControllerConnectionDialog.cpp b/src/gui/modals/ControllerConnectionDialog.cpp index 79daa25b565..4d1090d5c88 100644 --- a/src/gui/modals/ControllerConnectionDialog.cpp +++ b/src/gui/modals/ControllerConnectionDialog.cpp @@ -54,7 +54,7 @@ class AutoDetectMidiController : public MidiController AutoDetectMidiController( Model* parent ) : MidiController( parent ), m_detectedMidiChannel( 0 ), - m_detectedMidiController( 0 ) + m_detectedMidiController(NONE) { updateName(); } @@ -69,7 +69,7 @@ class AutoDetectMidiController : public MidiController ( m_midiPort.inputChannel() == 0 || m_midiPort.inputChannel() == event.channel() + 1 ) ) { m_detectedMidiChannel = event.channel() + 1; - m_detectedMidiController = event.controllerNumber() + 1; + m_detectedMidiController = event.controllerNumber(); m_detectedMidiPort = Engine::audioEngine()->midiClient()->sourcePortName( event ); emit valueChanged(); @@ -152,7 +152,7 @@ ControllerConnectionDialog::ControllerConnectionDialog( QWidget * _parent, m_midiControllerSpinBox = new LcdSpinBox( 3, m_midiGroupBox, tr( "Input controller" ) ); - m_midiControllerSpinBox->addTextForValue( 0, "---" ); + m_midiControllerSpinBox->addTextForValue(MidiController::NONE, "---" ); m_midiControllerSpinBox->setLabel( tr( "CONTROLLER" ) ); m_midiControllerSpinBox->move( 68, 24 ); diff --git a/src/gui/modals/FileDialog.cpp b/src/gui/modals/FileDialog.cpp index 512d7179f0d..a6cf4827a3e 100644 --- a/src/gui/modals/FileDialog.cpp +++ b/src/gui/modals/FileDialog.cpp @@ -26,11 +26,12 @@ #include #include #include +#include +#include #include "ConfigManager.h" #include "FileDialog.h" - namespace lmms::gui { @@ -45,19 +46,38 @@ FileDialog::FileDialog( QWidget *parent, const QString &caption, setOption( QFileDialog::DontUseNativeDialog ); - // Add additional locations to the sidebar +#ifdef LMMS_BUILD_LINUX + QList urls; +#else QList urls = sidebarUrls(); - urls << QUrl::fromLocalFile( QStandardPaths::writableLocation( QStandardPaths::DesktopLocation ) ); - // Find downloads directory - QDir downloadDir( QDir::homePath() + "/Downloads" ); - if ( ! downloadDir.exists() ) - downloadDir.setPath(QStandardPaths::writableLocation( QStandardPaths::DownloadLocation )); - if ( downloadDir.exists() ) - urls << QUrl::fromLocalFile( downloadDir.absolutePath() ); +#endif - urls << QUrl::fromLocalFile( QStandardPaths::writableLocation( QStandardPaths::MusicLocation ) ); - urls << QUrl::fromLocalFile( ConfigManager::inst()->workingDir() ); + QDir desktopDir; + desktopDir.setPath(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); + if (desktopDir.exists()) + { + urls << QUrl::fromLocalFile(desktopDir.absolutePath()); + } + + QDir downloadDir(QDir::homePath() + "/Downloads"); + if (!downloadDir.exists()) + { + downloadDir.setPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + } + if (downloadDir.exists()) + { + urls << QUrl::fromLocalFile(downloadDir.absolutePath()); + } + + QDir musicDir; + musicDir.setPath(QStandardPaths::writableLocation(QStandardPaths::MusicLocation)); + if (musicDir.exists()) + { + urls << QUrl::fromLocalFile(musicDir.absolutePath()); + } + urls << QUrl::fromLocalFile(ConfigManager::inst()->workingDir()); + // Add `/Volumes` directory on OS X systems, this allows the user to browse // external disk drives. #ifdef LMMS_BUILD_APPLE @@ -66,6 +86,22 @@ FileDialog::FileDialog( QWidget *parent, const QString &caption, urls << QUrl::fromLocalFile( volumesDir.absolutePath() ); #endif +#ifdef LMMS_BUILD_LINUX + + // FileSystem types : https://www.javatpoint.com/linux-file-system + QStringList usableFileSystems = {"ext", "ext2", "ext3", "ext4", "jfs", "reiserfs", "ntfs3", "fuse.sshfs", "fuseblk"}; + + for(QStorageInfo storage : QStorageInfo::mountedVolumes()) + { + storage.refresh(); + + if (usableFileSystems.contains(QString(storage.fileSystemType()), Qt::CaseInsensitive) && storage.isValid() && storage.isReady()) + { + urls << QUrl::fromLocalFile(storage.rootPath()); + } + } +#endif + setSidebarUrls(urls); } diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 63b84506e46..0266285a7a4 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -579,32 +579,39 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Buffer size tab. auto bufferSize_tw = new TabWidget(tr("Buffer size"), audio_w); - bufferSize_tw->setFixedHeight(76); + auto bufferSize_layout = new QVBoxLayout(bufferSize_tw); + bufferSize_layout->setSpacing(10); + bufferSize_layout->setContentsMargins(10, 18, 10, 10); m_bufferSizeSlider = new QSlider(Qt::Horizontal, bufferSize_tw); m_bufferSizeSlider->setRange(1, 128); m_bufferSizeSlider->setTickInterval(8); m_bufferSizeSlider->setPageStep(8); m_bufferSizeSlider->setValue(m_bufferSize / BUFFERSIZE_RESOLUTION); - m_bufferSizeSlider->setGeometry(10, 18, 340, 18); m_bufferSizeSlider->setTickPosition(QSlider::TicksBelow); + m_bufferSizeLbl = new QLabel(bufferSize_tw); + + m_bufferSizeWarnLbl = new QLabel(bufferSize_tw); + m_bufferSizeWarnLbl->setWordWrap(true); + connect(m_bufferSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBufferSize(int))); connect(m_bufferSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(showRestartWarning())); - - m_bufferSizeLbl = new QLabel(bufferSize_tw); - m_bufferSizeLbl->setGeometry(10, 40, 200, 24); setBufferSize(m_bufferSizeSlider->value()); auto bufferSize_reset_btn = new QPushButton(embed::getIconPixmap("reload"), "", bufferSize_tw); - bufferSize_reset_btn->setGeometry(320, 40, 28, 28); connect(bufferSize_reset_btn, SIGNAL(clicked()), this, SLOT(resetBufferSize())); bufferSize_reset_btn->setToolTip( tr("Reset to default value")); + bufferSize_layout->addWidget(m_bufferSizeSlider); + bufferSize_layout->addWidget(m_bufferSizeLbl); + bufferSize_layout->addWidget(m_bufferSizeWarnLbl); + bufferSize_layout->addWidget(bufferSize_reset_btn); + // Audio layout ordering. audio_layout->addWidget(audioiface_tw); @@ -1172,6 +1179,24 @@ void SetupDialog::audioInterfaceChanged(const QString & iface) } +void SetupDialog::updateBufferSizeWarning(int value) +{ + QString text = "
    "; + if((value & (value - 1)) != 0) // <=> value is not a power of 2 (for value > 0) + { + text += "
  • " + tr("The currently selected value is not a power of 2 " + "(32, 64, 128, 256, 512, 1024, ...). Some plugins may not be available.") + "
  • "; + } + if(value <= 32) + { + text += "
  • " + tr("The currently selected value is less than or equal to 32. " + "Some plugins may not be available.") + "
  • "; + } + text += "
"; + m_bufferSizeWarnLbl->setText(text); +} + + void SetupDialog::setBufferSize(int value) { const int step = DEFAULT_BUFFER_SIZE / BUFFERSIZE_RESOLUTION; @@ -1197,6 +1222,7 @@ void SetupDialog::setBufferSize(int value) m_bufferSize = value * BUFFERSIZE_RESOLUTION; m_bufferSizeLbl->setText(tr("Frames: %1\nLatency: %2 ms").arg(m_bufferSize).arg( 1000.0f * m_bufferSize / Engine::audioEngine()->processingSampleRate(), 0, 'f', 1)); + updateBufferSizeWarning(m_bufferSize); } diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 669fdaccb8f..87c0f044944 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -63,7 +63,6 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_tlb = new TrackLabelButton( this, getTrackSettingsWidget() ); m_tlb->setCheckable( true ); m_tlb->setIcon( embed::getIconPixmap( "instrument_track" ) ); - m_tlb->move( 3, 1 ); m_tlb->show(); connect( m_tlb, SIGNAL(toggled(bool)), @@ -75,24 +74,14 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV connect(ConfigManager::inst(), SIGNAL(valueChanged(QString,QString,QString)), this, SLOT(handleConfigChange(QString,QString,QString))); - // creation of widgets for track-settings-widget - int widgetWidth; - if( ConfigManager::inst()->value( "ui", - "compacttrackbuttons" ).toInt() ) - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT; - } - else - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH; - } + m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber->show(); m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), tr( "Volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_it->m_volumeModel ); m_volumeKnob->setHintText( tr( "Volume:" ), "%" ); - m_volumeKnob->move( widgetWidth-2*24, 2 ); m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); @@ -100,7 +89,6 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV tr( "Panning" ) ); m_panningKnob->setModel( &_it->m_panningModel ); m_panningKnob->setHintText(tr("Panning:"), "%"); - m_panningKnob->move( widgetWidth-24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -151,9 +139,18 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV QApplication::palette().color( QPalette::Active, QPalette::BrightText).darker(), getTrackSettingsWidget() ); - m_activityIndicator->setGeometry( - widgetWidth-2*24-11, 2, 8, 28 ); + m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); + + auto layout = new QHBoxLayout(getTrackSettingsWidget()); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tlb); + layout->addWidget(m_mixerChannelNumber); + layout->addWidget(m_activityIndicator); + layout->addWidget(m_volumeKnob); + layout->addWidget(m_panningKnob); + connect( m_activityIndicator, SIGNAL(pressed()), this, SLOT(activityIndicatorPressed())); connect( m_activityIndicator, SIGNAL(released()), @@ -268,6 +265,13 @@ void InstrumentTrackView::handleConfigChange(QString cls, QString attr, QString } } +void InstrumentTrackView::modelChanged() +{ + TrackView::modelChanged(); + auto st = castModel(); + m_mixerChannelNumber->setModel(&st->m_mixerChannelModel); +} + void InstrumentTrackView::dragEnterEvent( QDragEnterEvent * _dee ) { InstrumentTrackWindow::dragEnterEventGeneric( _dee ); diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 6a6a2c5fd2e..8516eb5c2a9 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -56,20 +56,17 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : connect(m_tlb, SIGNAL(clicked(bool)), this, SLOT(showEffects())); m_tlb->setIcon(embed::getIconPixmap("sample_track")); - m_tlb->move(3, 1); m_tlb->show(); + m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber->show(); + m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), tr( "Track volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_t->m_volumeModel ); m_volumeKnob->setHintText( tr( "Channel volume:" ), "%" ); - int settingsWidgetWidth = ConfigManager::inst()-> - value( "ui", "compacttrackbuttons" ).toInt() - ? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT - : DEFAULT_SETTINGS_WIDGET_WIDTH; - m_volumeKnob->move( settingsWidgetWidth - 2 * 24, 2 ); m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); @@ -77,7 +74,6 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : tr( "Panning" ) ); m_panningKnob->setModel( &_t->m_panningModel ); m_panningKnob->setHintText( tr( "Panning:" ), "%" ); - m_panningKnob->move( settingsWidgetWidth - 24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -87,8 +83,18 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : QApplication::palette().color(QPalette::Active, QPalette::BrightText).darker(), getTrackSettingsWidget() ); - m_activityIndicator->setGeometry(settingsWidgetWidth - 2 * 24 - 11, 2, 8, 28); + m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); + + auto layout = new QHBoxLayout(getTrackSettingsWidget()); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tlb); + layout->addWidget(m_mixerChannelNumber); + layout->addWidget(m_activityIndicator); + layout->addWidget(m_volumeKnob); + layout->addWidget(m_panningKnob); + connect(_t, SIGNAL(playingChanged()), this, SLOT(updateIndicator())); setModel( _t ); @@ -170,6 +176,7 @@ void SampleTrackView::modelChanged() { auto st = castModel(); m_volumeKnob->setModel(&st->m_volumeModel); + m_mixerChannelNumber->setModel(&st->m_mixerChannelModel); TrackView::modelChanged(); } diff --git a/src/gui/tracks/TrackContentWidget.cpp b/src/gui/tracks/TrackContentWidget.cpp index 442d717bfd1..619eff8317b 100644 --- a/src/gui/tracks/TrackContentWidget.cpp +++ b/src/gui/tracks/TrackContentWidget.cpp @@ -344,7 +344,7 @@ bool TrackContentWidget::canPasteSelection( TimePos clipPos, const QMimeData* md const int initialTrackIndex = tiAttr.value().toInt(); // Get the current track's index - const TrackContainer::TrackList tracks = t->trackContainer()->tracks(); + const TrackContainer::TrackList& tracks = t->trackContainer()->tracks(); const auto currentTrackIt = std::find(tracks.begin(), tracks.end(), t); const int currentTrackIndex = currentTrackIt != tracks.end() ? std::distance(tracks.begin(), currentTrackIt) : -1; @@ -443,7 +443,7 @@ bool TrackContentWidget::pasteSelection( TimePos clipPos, const QMimeData * md, TimePos grabbedClipPos = clipPosAttr.value().toInt(); // Snap the mouse position to the beginning of the dropped bar, in ticks - const TrackContainer::TrackList tracks = getTrack()->trackContainer()->tracks(); + const TrackContainer::TrackList& tracks = getTrack()->trackContainer()->tracks(); const auto currentTrackIt = std::find(tracks.begin(), tracks.end(), getTrack()); const int currentTrackIndex = currentTrackIt != tracks.end() ? std::distance(tracks.begin(), currentTrackIt) : -1; diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index ce6177d76da..fa1a651f613 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -64,7 +64,6 @@ TrackOperationsWidget::TrackOperationsWidget( TrackView * parent ) : "to begin a new drag'n'drop action." ).arg(UI_CTRL_KEY) ); auto toMenu = new QMenu(this); - toMenu->setFont( pointSize<9>( toMenu->font() ) ); connect( toMenu, SIGNAL(aboutToShow()), this, SLOT(updateMenu())); diff --git a/src/gui/widgets/CPULoadWidget.cpp b/src/gui/widgets/CPULoadWidget.cpp index 799e037ef43..db1f5cacc8f 100644 --- a/src/gui/widgets/CPULoadWidget.cpp +++ b/src/gui/widgets/CPULoadWidget.cpp @@ -24,6 +24,7 @@ */ +#include #include #include "AudioEngine.h" @@ -72,10 +73,9 @@ void CPULoadWidget::paintEvent( QPaintEvent * ) QPainter p( &m_temp ); p.drawPixmap( 0, 0, m_background ); - // as load-indicator consists of small 2-pixel wide leds with - // 1 pixel spacing, we have to make sure, only whole leds are - // shown which we achieve by the following formula - int w = ( m_leds.width() * m_currentLoad / 300 ) * 3; + // Normally the CPU load indicator moves smoothly, with 1 pixel resolution. However, some themes may want to + // draw discrete elements (like LEDs), so the stepSize property can be used to specify a larger step size. + int w = (m_leds.width() * std::min(m_currentLoad, 100) / (stepSize() * 100)) * stepSize(); if( w > 0 ) { p.drawPixmap( 23, 3, m_leds, 0, 0, w, @@ -91,10 +91,21 @@ void CPULoadWidget::paintEvent( QPaintEvent * ) void CPULoadWidget::updateCpuLoad() { - // smooth load-values a bit - int new_load = ( m_currentLoad + Engine::audioEngine()->cpuLoad() ) / 2; - if( new_load != m_currentLoad ) + // Additional display smoothing for the main load-value. Stronger averaging + // cannot be used directly in the profiler: cpuLoad() must react fast enough + // to be useful as overload indicator in AudioEngine::criticalXRuns(). + const int new_load = (m_currentLoad + Engine::audioEngine()->cpuLoad()) / 2; + + if (new_load != m_currentLoad) { + auto engine = Engine::audioEngine(); + setToolTip( + tr("DSP total: %1%").arg(new_load) + "\n" + + tr(" - Notes and setup: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::NoteSetup)) + "\n" + + tr(" - Instruments: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Instruments)) + "\n" + + tr(" - Effects: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Effects)) + "\n" + + tr(" - Mixing: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Mixing)) + ); m_currentLoad = new_load; m_changed = true; update(); @@ -102,4 +113,4 @@ void CPULoadWidget::updateCpuLoad() } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index bdf78ccce36..2377a37abf8 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -70,7 +70,6 @@ ComboBox::ComboBox( QWidget * _parent, const QString & _name ) : } setFont( pointSize<9>( font() ) ); - m_menu.setFont( pointSize<8>( m_menu.font() ) ); connect( &m_menu, SIGNAL(triggered(QAction*)), this, SLOT(setItem(QAction*))); diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index c2f90fb2b71..56cf29345d0 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -22,6 +22,8 @@ * */ +#include "Knob.h" + #include #include #include @@ -34,7 +36,6 @@ #endif #include "lmms_math.h" -#include "Knob.h" #include "CaptionMenu.h" #include "ConfigManager.h" #include "ControllerConnection.h" @@ -48,7 +49,6 @@ #include "SimpleTextFloat.h" #include "StringPairDrag.h" - namespace lmms::gui { @@ -484,6 +484,13 @@ void Knob::drawKnob( QPainter * _p ) _p->drawImage( 0, 0, m_cache ); } +void Knob::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) +{ + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime); +} + float Knob::getValue( const QPoint & _p ) { float value; @@ -580,10 +587,8 @@ void Knob::mousePressEvent( QMouseEvent * _me ) emit sliderPressed(); - s_textFloat->setText( displayValue() ); - s_textFloat->moveGlobal( this, - QPoint( width() + 2, 0 ) ); - s_textFloat->show(); + showTextFloat(0, 0); + m_buttonPressed = true; } else if( _me->button() == Qt::LeftButton && @@ -613,6 +618,7 @@ void Knob::mouseMoveEvent( QMouseEvent * _me ) m_lastMousePos = _me->pos(); } s_textFloat->setText( displayValue() ); + s_textFloat->show(); } @@ -638,7 +644,15 @@ void Knob::mouseReleaseEvent( QMouseEvent* event ) s_textFloat->hide(); } +void Knob::enterEvent(QEvent *event) +{ + showTextFloat(700, 2000); +} +void Knob::leaveEvent(QEvent *event) +{ + s_textFloat->hide(); +} void Knob::focusOutEvent( QFocusEvent * _fe ) diff --git a/src/gui/widgets/LcdFloatSpinBox.cpp b/src/gui/widgets/LcdFloatSpinBox.cpp index 6391f314ad9..96f2b27e1db 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -49,6 +49,7 @@ namespace lmms::gui LcdFloatSpinBox::LcdFloatSpinBox(int numWhole, int numFrac, const QString& name, QWidget* parent) : + QWidget(parent), FloatModelView(new FloatModel(0, 0, 0, 0, nullptr, name, true), this), m_wholeDisplay(numWhole, parent, name, false), m_fractionDisplay(numFrac, parent, name, true), @@ -62,6 +63,7 @@ LcdFloatSpinBox::LcdFloatSpinBox(int numWhole, int numFrac, const QString& name, LcdFloatSpinBox::LcdFloatSpinBox(int numWhole, int numFrac, const QString& style, const QString& name, QWidget* parent) : + QWidget(parent), FloatModelView(new FloatModel(0, 0, 0, 0, nullptr, name, true), this), m_wholeDisplay(numWhole, style, parent, name, false), m_fractionDisplay(numFrac, style, parent, name, true), @@ -101,6 +103,7 @@ void LcdFloatSpinBox::layoutSetup(const QString &style) outerLayout->setContentsMargins(0, 0, 0, 0); outerLayout->setSizeConstraint(QLayout::SetFixedSize); this->setLayout(outerLayout); + this->setFixedHeight(32); } @@ -240,9 +243,9 @@ void LcdFloatSpinBox::paintEvent(QPaintEvent*) { p.setFont(pointSizeF(p.font(), 6.5)); p.setPen(m_wholeDisplay.textShadowColor()); - p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2 + 1, height(), m_label); + p.drawText(width() / 2 - p.fontMetrics().boundingRect(m_label).width() / 2 + 1, height(), m_label); p.setPen(m_wholeDisplay.textColor()); - p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2, height() - 1, m_label); + p.drawText(width() / 2 - p.fontMetrics().boundingRect(m_label).width() / 2, height() - 1, m_label); } } diff --git a/src/gui/widgets/SimpleTextFloat.cpp b/src/gui/widgets/SimpleTextFloat.cpp index d1f490b5ef9..e37753229ac 100644 --- a/src/gui/widgets/SimpleTextFloat.cpp +++ b/src/gui/widgets/SimpleTextFloat.cpp @@ -45,6 +45,14 @@ SimpleTextFloat::SimpleTextFloat() : m_textLabel = new QLabel(this); layout->addWidget(m_textLabel); + + m_showTimer = new QTimer(this); + m_showTimer->setSingleShot(true); + QObject::connect(m_showTimer, &QTimer::timeout, this, &SimpleTextFloat::show); + + m_hideTimer = new QTimer(this); + m_hideTimer->setSingleShot(true); + QObject::connect(m_hideTimer, &QTimer::timeout, this, &SimpleTextFloat::hide); } void SimpleTextFloat::setText(const QString & text) @@ -52,6 +60,29 @@ void SimpleTextFloat::setText(const QString & text) m_textLabel->setText(text); } +void SimpleTextFloat::showWithDelay(int msecBeforeDisplay, int msecDisplayTime) +{ + if (msecBeforeDisplay != 0) + { + m_showTimer->start(msecBeforeDisplay); + } + else + { + show(); + } + + if (msecDisplayTime != 0) + { + m_hideTimer->start(msecBeforeDisplay + msecDisplayTime); + } +} + +void SimpleTextFloat::hide() +{ + m_showTimer->stop(); + m_hideTimer->stop(); + QWidget::hide(); +} void SimpleTextFloat::setVisibilityTimeOut(int msecs) { diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 84b73614b86..29fda075e9c 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -28,6 +28,7 @@ #include "ConfigManager.h" #include "ControllerConnection.h" #include "DataFile.h" +#include "GuiApplication.h" #include "Mixer.h" #include "InstrumentTrackView.h" #include "Instrument.h" @@ -37,6 +38,7 @@ #include "MixHelpers.h" #include "PatternStore.h" #include "PatternTrack.h" +#include "PianoRoll.h" #include "Pitch.h" #include "Song.h" @@ -72,6 +74,7 @@ InstrumentTrack::InstrumentTrack( TrackContainer* tc ) : m_microtuner() { m_pitchModel.setCenterValue( 0 ); + m_pitchModel.setStrictStepSize(true); m_panningModel.setCenterValue( DefaultPanning ); m_baseNoteModel.setInitValue( DefaultKey ); m_firstKeyModel.setInitValue(0); @@ -341,9 +344,10 @@ void InstrumentTrack::processInEvent( const MidiEvent& event, const TimePos& tim NotePlayHandleManager::acquire( this, offset, typeInfo::max() / 2, - Note( TimePos(), TimePos(), event.key(), event.volume( midiPort()->baseVelocity() ) ), + Note(TimePos(), Engine::getSong()->getPlayPos(Engine::getSong()->playMode()), + event.key(), event.volume(midiPort()->baseVelocity())), nullptr, event.channel(), - NotePlayHandle::Origin::MidiInput ); + NotePlayHandle::Origin::MidiInput); m_notes[event.key()] = nph; if( ! Engine::audioEngine()->addPlayHandle( nph ) ) { @@ -566,6 +570,10 @@ f_cnt_t InstrumentTrack::beatLen( NotePlayHandle * _n ) const void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) { + // Note: under certain circumstances the working buffer is a nullptr. + // These cases are triggered in PlayHandle::doProcessing when the play method is called with a nullptr. + // TODO: Find out if we can skip processing at a higher level if the buffer is nullptr. + // arpeggio- and chord-widget has to do its work -> adding sub-notes // for chords/arpeggios m_noteStacking.processNote( n ); @@ -575,6 +583,15 @@ void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) { // all is done, so now lets play the note! m_instrument->playNote( n, workingBuffer ); + + // This is effectively the same as checking if workingBuffer is not a nullptr. + // Calling processAudioBuffer with a nullptr leads to crashes. Hence the check. + if (n->usesBuffer()) + { + const fpp_t frames = n->framesLeftForCurrentPeriod(); + const f_cnt_t offset = n->noteOffset(); + processAudioBuffer(workingBuffer, frames + offset, n); + } } } @@ -710,7 +727,7 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, // Handle automation: detuning for (const auto& processHandle : m_processHandles) { - processHandle->processTimePos(_start); + processHandle->processTimePos(_start, m_pitchModel.value(), gui::GuiApplication::instance()->pianoRoll()->isRecording()); } if ( clips.size() == 0 ) diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index b35979f61f8..490f6e6d041 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -305,7 +305,7 @@ void MidiClip::setStep( int step, bool enabled ) -void MidiClip::splitNotes(NoteVector notes, TimePos pos) +void MidiClip::splitNotes(const NoteVector& notes, TimePos pos) { if (notes.empty()) { return; } @@ -472,7 +472,7 @@ MidiClip * MidiClip::nextMidiClip() const MidiClip * MidiClip::adjacentMidiClipByOffset(int offset) const { - std::vector clips = m_instrumentTrack->getClips(); + auto& clips = m_instrumentTrack->getClips(); int clipNum = m_instrumentTrack->getClipNum(this); if (clipNum < 0 || clipNum > clips.size() - 1) { return nullptr; } return dynamic_cast(clips[clipNum + offset]); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6ff9c41e967..ddf9e29621b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,7 +19,9 @@ ADD_EXECUTABLE(tests QTestSuite.cpp $ + src/core/ArrayVectorTest.cpp src/core/AutomatableModelTest.cpp + src/core/MathTest.cpp src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp diff --git a/tests/main.cpp b/tests/main.cpp index 6d375e6c657..c1a5b5a1016 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -16,7 +16,7 @@ int main(int argc, char* argv[]) int failed = 0; for (QTestSuite*& suite : QTestSuite::suites()) { - failed += QTest::qExec(suite, argc, argv); + if (QTest::qExec(suite, argc, argv) != 0) { ++failed; } } qDebug() << "<<" << failed << "out of"< +#include + +#include "QTestSuite.h" + +using lmms::ArrayVector; + +struct ShouldNotConstruct +{ + ShouldNotConstruct() { QFAIL("should not construct"); } +}; + +struct ShouldNotDestruct +{ + ~ShouldNotDestruct() { QFAIL("should not destruct"); } +}; + +enum class Construction { Default, Copy, Move, CopyAssign, MoveAssign }; + +struct Constructible +{ + Constructible() : construction{Construction::Default} {} + Constructible(const Constructible&) : construction{Construction::Copy} {} + Constructible(Constructible&&) : construction{Construction::Move} {} + Constructible& operator=(const Constructible&) { construction = Construction::CopyAssign; return *this; } + Constructible& operator=(Constructible&&) { construction = Construction::MoveAssign; return *this; } + Construction construction; +}; + +struct DestructorCheck +{ + ~DestructorCheck() { *destructed = true; } + bool* destructed; +}; + +class ArrayVectorTest : QTestSuite +{ + Q_OBJECT + +private slots: + void defaultConstructorTest() + { + // Ensure no elements are constructed + const auto v = ArrayVector(); + // Ensure the container is empty + QVERIFY(v.empty()); + } + + void copyConstructorTest() + { + { + // Ensure all elements are copy constructed + const auto v = ArrayVector{{}}; + const auto copy = v; + for (const auto& element : copy) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + const auto v = ArrayVector{1, 2, 3}; + const auto copy = v; + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(copy.begin(), copy.end(), expected.begin(), expected.end())); + } + } + + void moveConstructorTest() + { + { + // Ensure all elements are move constructed + auto v = ArrayVector{{}}; + const auto moved = std::move(v); + for (const auto& element : moved) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{1, 2, 3}; + const auto moved = std::move(v); + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(moved.begin(), moved.end(), expected.begin(), expected.end())); + // Move construction should leave the source empty + QVERIFY(v.empty()); + } + } + + void fillValueConstructorTest() + { + // Ensure all elements are copy constructed + const auto v = ArrayVector(1, {}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + // Ensure the container has the correct size + QCOMPARE(v.size(), std::size_t{1}); + } + + void fillDefaultConstructorTest() + { + // Ensure all elements are copy constructed + const auto v = ArrayVector(1); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Default); + } + // Ensure the container has the correct size + QCOMPARE(v.size(), std::size_t{1}); + } + + void rangeConstructorTest() + { + { + // Ensure the elements are copy constructed from normal iterators + const auto data = std::array{Constructible{}}; + const auto v = ArrayVector(data.begin(), data.end()); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure the elements are move constructed from move iterators + auto data = std::array{Constructible{}}; + const auto v = ArrayVector( + std::move_iterator{data.begin()}, std::move_iterator{data.end()}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + const auto data = std::array{1, 2, 3}; + const auto v = ArrayVector(data.begin(), data.end()); + QVERIFY(std::equal(v.begin(), v.end(), data.begin(), data.end())); + } + } + + void initializerListConstructorTest() + { + // Ensure the container is constructed with the correct data + const auto v = ArrayVector{1, 2, 3}; + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + } + + void destructorTest() + { + { + // Should not call destructors for space without elements + const auto v = ArrayVector{}; + } + { + // Should call destructors for all elements + auto destructed = false; + { + const auto v = ArrayVector{{&destructed}}; + } + QVERIFY(destructed); + } + } + + void copyAssignmentTest() + { + { + // Self-assignment should not change the contents + auto v = ArrayVector{1, 2, 3}; + const auto oldValue = v; + v = v; + QCOMPARE(v, oldValue); + } + { + // Assignment to a larger container should copy assign + const auto src = ArrayVector(3); + auto dst = ArrayVector(5); + dst = src; + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + const auto src = ArrayVector(3); + auto dst = ArrayVector{}; + dst = src; + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + const auto src = ArrayVector{1, 2, 3}; + auto dst = ArrayVector{}; + dst = src; + QCOMPARE(dst, (ArrayVector{1, 2, 3})); + } + } + + void moveAssignmentTest() + { + { + // Self-assignment should not change the contents + auto v = ArrayVector{1, 2, 3}; + const auto oldValue = v; + v = std::move(v); + QCOMPARE(v, oldValue); + } + { + // Assignment to a larger container should move assign + auto src = ArrayVector(3); + auto dst = ArrayVector(5); + dst = std::move(src); + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::MoveAssign); + } + } + { + // Assignment to a smaller container should move construct + auto src = ArrayVector(3); + auto dst = ArrayVector{}; + dst = std::move(src); + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + auto src = ArrayVector{1, 2, 3}; + auto dst = ArrayVector{}; + dst = std::move(src); + QCOMPARE(dst, (ArrayVector{1, 2, 3})); + } + } + + void initializerListAssignmentTest() + { + { + // Assignment to a larger container should copy assign + auto v = ArrayVector(2); + v = {Constructible{}}; + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + auto v = ArrayVector{}; + v = {Constructible{}}; + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{}; + v = {1, 2, 3}; + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void fillValueAssignTest() + { + { + // Assignment to a larger container should copy assign + auto v = ArrayVector(5); + v.assign(3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + auto v = ArrayVector{}; + v.assign(3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + auto v = ArrayVector{}; + v.assign(3, 1); + QCOMPARE(v, (ArrayVector{1, 1, 1})); + } + } + + void rangeAssignTest() + { + { + // Assignment to a larger container should copy assign + const auto data = std::array{Constructible{}}; + auto v = ArrayVector(2); + v.assign(data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + const auto data = std::array{Constructible{}}; + auto v = ArrayVector{}; + v.assign(data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + const auto data = std::array{1, 2, 3}; + auto v = ArrayVector{}; + v.assign(data.begin(), data.end()); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void atTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.at(1), 2); + QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.at(1), 2); + QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range); + } + } + + void subscriptTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v[1], 2); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v[1], 2); + } + } + + void frontTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.front(), 1); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.front(), 1); + } + } + + void backTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.back(), 3); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.back(), 3); + } + } + + void dataTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.data(), &v.front()); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.data(), &v.front()); + } + } + + void beginEndTest() + { + const auto expected = std::array{1, 2, 3}; + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + QVERIFY(std::equal(v.cbegin(), v.cend(), expected.begin(), expected.end())); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + } + } + + void rbeginRendTest() + { + const auto expected = std::array{3, 2, 1}; + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end())); + QVERIFY(std::equal(v.crbegin(), v.crend(), expected.begin(), expected.end())); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end())); + } + } + + void emptyFullSizeMaxCapacityTest() + { + auto v = ArrayVector{}; + QVERIFY(v.empty()); + QVERIFY(!v.full()); + QCOMPARE(v.size(), std::size_t{0}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + v.push_back(1); + QVERIFY(!v.empty()); + QVERIFY(!v.full()); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + v.push_back(2); + QVERIFY(!v.empty()); + QVERIFY(v.full()); + QCOMPARE(v.size(), std::size_t{2}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + auto empty = ArrayVector{}; + QVERIFY(empty.empty()); + QVERIFY(empty.full()); + QCOMPARE(empty.size(), std::size_t{0}); + QCOMPARE(empty.max_size(), std::size_t{0}); + QCOMPARE(empty.capacity(), std::size_t{0}); + } + + void insertValueTest() + { + { + // Copy + const auto data = Constructible{}; + auto v = ArrayVector{}; + v.insert(v.cbegin(), data); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Move + auto v = ArrayVector{}; + v.insert(v.cbegin(), Constructible{}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Move); + } + { + // Ensure the correct value is used (copy) + const auto data = 1; + auto v = ArrayVector{2, 3}; + v.insert(v.cbegin(), data); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{2, 3}; + v.insert(v.cbegin(), 1); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void insertFillValueTest() + { + { + // Insertion should copy construct + auto v = ArrayVector{}; + v.insert(v.cbegin(), 3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + auto v = ArrayVector{1, 3}; + v.insert(v.cbegin() + 1, 3, 2); + QCOMPARE(v, (ArrayVector{1, 2, 2, 2, 3})); + } + } + + void insertRangeTest() + { + { + // Insertion should copy construct + const auto data = std::array{Constructible{}}; + auto v = ArrayVector{}; + v.insert(v.cbegin(), data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + const auto data = std::array{2, 3}; + auto v = ArrayVector{1, 4}; + v.insert(v.cbegin() + 1, data.begin(), data.end()); + QCOMPARE(v, (ArrayVector{1, 2, 3, 4})); + } + } + + void insertInitializerListTest() + { + { + // Insertion should copy construct + auto v = ArrayVector{}; + v.insert(v.cbegin(), {Constructible{}}); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{1, 4}; + v.insert(v.cbegin() + 1, {2, 3}); + QCOMPARE(v, (ArrayVector{1, 2, 3, 4})); + } + } + + void emplaceTest() + { + { + // Ensure the value is constructed in-place + auto v = ArrayVector{}; + v.emplace(v.cbegin()); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{2, 3}; + v.emplace(v.cbegin(), 1); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void eraseTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.erase(v.cbegin()); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{10, 1, 2, 3}; + v.erase(v.cbegin()); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void eraseRangeTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.erase(v.cbegin(), v.cend()); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 20, 21, 2, 3}; + v.erase(v.cbegin() + 1, v.cbegin() + 3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void pushBackTest() + { + { + // Copy + const auto data = Constructible{}; + auto v = ArrayVector{}; + v.push_back(data); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Move + auto v = ArrayVector{}; + v.push_back({}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Move); + } + { + // Ensure the correct value is used (copy) + const auto data = 3; + auto v = ArrayVector{1, 2}; + v.push_back(data); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{1, 2}; + v.push_back(3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void emplaceBackTest() + { + { + // Ensure the value is constructed in-place + auto v = ArrayVector{}; + v.emplace_back(); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{1, 2}; + v.emplace_back(3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void popBackTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.pop_back(); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 2, 3}; + v.pop_back(); + QCOMPARE(v, (ArrayVector{1, 2})); + } + } + + void resizeDefaultTest() + { + { + // Smaller + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + QCOMPARE(v.size(), std::size_t{1}); + v.resize(0); + QCOMPARE(v.size(), std::size_t{0}); + QVERIFY(destructed); + } + { + // Bigger + auto v = ArrayVector{}; + QCOMPARE(v.size(), std::size_t{0}); + v.resize(1); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Too big + auto v = ArrayVector{}; + QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error); + } + } + + void resizeValueTest() + { + { + // Smaller + auto dummy = false; + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + QCOMPARE(v.size(), std::size_t{1}); + v.resize(0, {&dummy}); + QCOMPARE(v.size(), std::size_t{0}); + QVERIFY(destructed); + } + { + // Bigger + auto v = ArrayVector{}; + QCOMPARE(v.size(), std::size_t{0}); + v.resize(1, {}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Too big + auto v = ArrayVector{}; + QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error); + } + { + // Ensure the correct value is used + auto v = ArrayVector{}; + v.resize(1, 1); + QCOMPARE(v, (ArrayVector{1})); + } + } + + void clearTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.clear(); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 2, 3}; + v.clear(); + QCOMPARE(v, (ArrayVector{})); + } + } + + void memberSwapTest() + { + auto a = ArrayVector{1, 2, 3, 4}; + auto b = ArrayVector{2, 4, 6}; + + const auto aOriginal = a; + const auto bOriginal = b; + + a.swap(b); + + QCOMPARE(a, bOriginal); + QCOMPARE(b, aOriginal); + } + + void freeSwapTest() + { + auto a = ArrayVector{1, 2, 3, 4}; + auto b = ArrayVector{2, 4, 6}; + + const auto aOriginal = a; + const auto bOriginal = b; + + swap(a, b); + + QCOMPARE(a, bOriginal); + QCOMPARE(b, aOriginal); + } + + void comparisonTest() + { + const auto v = ArrayVector{1, 2, 3}; + const auto l = ArrayVector{1, 2, 2}; + const auto e = ArrayVector{1, 2, 3}; + const auto g = ArrayVector{1, 3, 3}; + + QVERIFY(l < v); + QVERIFY(!(e < v)); + QVERIFY(!(g < v)); + + QVERIFY(l <= v); + QVERIFY(e <= v); + QVERIFY(!(g <= v)); + + QVERIFY(!(l > v)); + QVERIFY(!(e > v)); + QVERIFY(g > v); + + QVERIFY(!(l >= v)); + QVERIFY(e >= v); + QVERIFY(g >= v); + + QVERIFY(!(l == v)); + QVERIFY(e == v); + QVERIFY(!(g == v)); + + QVERIFY(l != v); + QVERIFY(!(e != v)); + QVERIFY(g != v); + } +} ArrayVectorTests; + +#include "ArrayVectorTest.moc" diff --git a/tests/src/core/MathTest.cpp b/tests/src/core/MathTest.cpp new file mode 100644 index 00000000000..2b6404cfd5c --- /dev/null +++ b/tests/src/core/MathTest.cpp @@ -0,0 +1,53 @@ +/* + * MathTest.cpp + * + * Copyright (c) 2023 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 "QTestSuite.h" + +#include "lmms_math.h" + +#include + +class MathTest : QTestSuite +{ + Q_OBJECT +private slots: + void NumDigitsTest() + { + using namespace lmms; + QCOMPARE(numDigitsAsInt(1.f), 1); + QCOMPARE(numDigitsAsInt(9.9f), 2); + QCOMPARE(numDigitsAsInt(10.f), 2); + QCOMPARE(numDigitsAsInt(0.f), 1); + QCOMPARE(numDigitsAsInt(-100.f), 4); + QCOMPARE(numDigitsAsInt(-99.f), 3); + QCOMPARE(numDigitsAsInt(-0.4f), 1); // there is no "-0" for LED spinbox + QCOMPARE(numDigitsAsInt(-0.99f), 2); + QCOMPARE(numDigitsAsInt(1000000000), 10); + QCOMPARE(numDigitsAsInt(-1000000000), 11); + QCOMPARE(numDigitsAsInt(900000000), 9); + QCOMPARE(numDigitsAsInt(-900000000), 10); + } +} MathTests; + +#include "MathTest.moc"