diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 007842b82ad..5fe1ec7bf72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,8 @@ jobs: -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_COMPILE_CACHE=ON - CCACHE_MAXSIZE: 500M + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j2 steps: - name: Update and configure Git @@ -38,6 +39,7 @@ jobs: path: ~/.ccache - name: Configure run: | + ccache --zero-stats source /opt/qt5*/bin/qt5*-env.sh || true mkdir build && cd build cmake .. $CMAKE_OPTS -DCMAKE_INSTALL_PREFIX=./install @@ -56,29 +58,42 @@ jobs: with: name: linux path: build/lmms-*.AppImage - - name: Print ccache statistics + - name: Trim ccache and print statistics run: | + ccache --cleanup echo "[ccache config]" - ccache -p + ccache --print-config echo "[ccache stats]" - ccache -s + ccache --show-stats + env: + CCACHE_MAXSIZE: 500M macos: name: macos - runs-on: macos-11 + runs-on: macos-12 env: CMAKE_OPTS: >- -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_COMPILE_CACHE=ON - CCACHE_MAXSIZE: 500M + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j3 - DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_13.1.app/Contents/Developer steps: - name: Check out uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive + - name: Clean up Homebrew download cache + run: rm -rf ~/Library/Caches/Homebrew/downloads + - name: Restore Homebrew download cache + uses: actions/cache/restore@v3 + with: + key: n/a - only restore from restore-keys + restore-keys: | + homebrew- + path: ~/Library/Caches/Homebrew/downloads - name: Cache ccache data uses: actions/cache@v3 with: @@ -89,12 +104,16 @@ jobs: path: ~/Library/Caches/ccache - name: Install dependencies run: | - brew install ccache fftw pkg-config libogg libvorbis lame libsndfile \ - libsamplerate jack sdl libgig libsoundio lilv lv2 stk \ - fluid-synth portaudio fltk qt@5 carla + brew bundle install --verbose + npm update -g npm npm install --location=global appdmg + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALL_UPGRADE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - name: Configure run: | + ccache --zero-stats mkdir build cmake -S . \ -B build \ @@ -117,12 +136,20 @@ jobs: with: name: macos path: build/lmms-*.dmg - - name: Print ccache statistics + - name: Trim ccache and print statistics run: | + ccache --cleanup echo "[ccache config]" - ccache -p + ccache --show-config echo "[ccache stats]" - ccache -s + ccache --show-stats --verbose + env: + CCACHE_MAXSIZE: 500MB + - name: Save Homebrew download cache + uses: actions/cache/save@v3 + with: + key: homebrew-${{ hashFiles('Brewfile.lock.json') }} + path: ~/Library/Caches/Homebrew/downloads mingw: strategy: fail-fast: false @@ -136,7 +163,8 @@ jobs: -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_COMPILE_CACHE=ON - CCACHE_MAXSIZE: 500M + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j2 steps: - name: Update and configure Git @@ -161,6 +189,7 @@ jobs: path: ~/.ccache - name: Configure run: | + ccache --zero-stats mkdir build && cd build ../cmake/build_win${{ matrix.arch }}.sh - name: Build @@ -174,12 +203,15 @@ jobs: with: name: mingw${{ matrix.arch }} path: build/lmms-*.exe - - name: Print ccache statistics + - name: Trim ccache and print statistics run: | + ccache --cleanup echo "[ccache config]" - ccache -p + ccache --print-config echo "[ccache stats]" - ccache -s + ccache --show-stats + env: + CCACHE_MAXSIZE: 500M msvc: strategy: fail-fast: false @@ -189,6 +221,8 @@ jobs: runs-on: windows-2019 env: qt-version: '5.15.2' + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 steps: - name: Check out uses: actions/checkout@v3 @@ -196,49 +230,60 @@ jobs: fetch-depth: 0 submodules: recursive - name: Cache vcpkg dependencies + id: cache-deps uses: actions/cache@v3 with: - key: vcpkg-${{ matrix.arch }}-${{ github.ref }}-${{ github.run_id }} + key: vcpkg-${{ matrix.arch }}-${{ hashFiles('vcpkg.json') }} restore-keys: | - vcpkg-${{ matrix.arch }}-${{ github.ref }}- vcpkg-${{ matrix.arch }}- - path: C:\vcpkg\installed + path: build\vcpkg_installed + - name: Cache ccache data + uses: actions/cache@v3 + with: + key: "ccache-${{ github.job }}-${{ matrix.arch }}-${{ github.ref }}\ + -${{ github.run_id }}" + restore-keys: | + ccache-${{ github.job }}-${{ matrix.arch }}-${{ github.ref }}- + ccache-${{ github.job }}-${{ matrix.arch }}- + path: ~\AppData\Local\ccache + - name: Install tools + run: choco install ccache - name: Install 64-bit Qt if: matrix.arch == 'x64' - uses: jurplel/install-qt-action@64bdb64f2c14311d23733a8463e5fcbc65e8775e + uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946 with: version: ${{ env.qt-version }} arch: win64_msvc2019_64 archives: qtbase qtsvg qttools cache: true - name: Install 32-bit Qt - uses: jurplel/install-qt-action@64bdb64f2c14311d23733a8463e5fcbc65e8775e + uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946 with: version: ${{ env.qt-version }} arch: win32_msvc2019 archives: qtbase qtsvg qttools cache: true set-env: ${{ matrix.arch == 'x86' }} - - name: Install dependencies - run: | - vcpkg install ` - --triplet=${{ matrix.arch }}-windows ` - --host-triplet=${{ matrix.arch }}-windows ` - --recurse ` - fftw3 fltk fluidsynth[sndfile] libsamplerate libsndfile libstk ` - lilv lv2 portaudio sdl2 - name: Set up build environment - uses: ilammy/msvc-dev-cmd@d8610e2b41c6d0f0c3b4c46dad8df0fd826c68e1 + uses: ilammy/msvc-dev-cmd@cec98b9d092141f74527d0afa6feb2af698cfe89 with: arch: ${{ matrix.arch }} - name: Configure run: | - mkdir build + ccache --zero-stats + mkdir build -Force cmake -S . ` -B build ` -G Ninja ` --toolchain C:/vcpkg/scripts/buildsystems/vcpkg.cmake ` - -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` + -DUSE_COMPILE_CACHE=ON ` + -DVCPKG_TARGET_TRIPLET="${{ matrix.arch }}-windows" ` + -DVCPKG_HOST_TRIPLET="${{ matrix.arch }}-windows" ` + -DVCPKG_MANIFEST_INSTALL="${{ env.should_install_manifest }}" + env: + should_install_manifest: + ${{ steps.cache-deps.outputs.cache-hit == 'true' && 'NO' || 'YES' }} - name: Build run: cmake --build build - name: Build tests @@ -250,3 +295,12 @@ jobs: with: name: msvc-${{ matrix.arch }} path: build\lmms-*.exe + - name: Trim ccache and print statistics + run: | + ccache --cleanup + echo "[ccache config]" + ccache --show-config + echo "[ccache stats]" + ccache --show-stats --verbose + env: + CCACHE_MAXSIZE: 500MB diff --git a/.gitignore b/.gitignore index ee289379f0e..1b855f204cb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /plugins/ZynAddSubFx/zynaddsubfx/doc/Makefile /plugins/ZynAddSubFx/zynaddsubfx/doc/gen/Makefile /data/locale/*.qm +Brewfile.lock.json diff --git a/Brewfile b/Brewfile new file mode 100644 index 00000000000..1bfbd7b01c1 --- /dev/null +++ b/Brewfile @@ -0,0 +1,20 @@ +brew "carla" +brew "ccache" +brew "fftw" +brew "fltk" +brew "fluid-synth" +brew "jack" +brew "lame" +brew "libgig" +brew "libogg" +brew "libsamplerate" +brew "libsndfile" +brew "libsoundio" +brew "libvorbis" +brew "lilv" +brew "lv2" +brew "pkg-config" +brew "portaudio" +brew "qt@5" +brew "sdl2" +brew "stk" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b6d3b4ff82..858849abd2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,31 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.9) +# Set the given policy to NEW. If it does not exist, it will not be set. If it +# is already set to NEW (most likely due to predating the minimum required CMake +# version), a developer warning is emitted indicating that the policy need no +# longer be explicitly set. +function(enable_policy_if_exists id) + if(POLICY "${id}") + cmake_policy(GET "${id}" current_value) + if(current_value STREQUAL "NEW") + message(AUTHOR_WARNING "${id} is now set to NEW by default, and no longer needs to be explicitly set.") + else() + cmake_policy(SET "${id}" NEW) + endif() + endif() +endfunction() + +# Needed for the SWH Ladspa plugins. See below. +enable_policy_if_exists(CMP0074) # find_package() uses _ROOT variables. +# Needed for ccache support with MSVC +enable_policy_if_exists(CMP0141) # MSVC debug information format flags are selected by an abstraction. + PROJECT(lmms) SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules" ${CMAKE_MODULE_PATH}) SET(LMMS_BINARY_DIR ${CMAKE_BINARY_DIR}) SET(LMMS_SOURCE_DIR ${CMAKE_SOURCE_DIR}) -# CMAKE_POLICY Section -IF(COMMAND CMAKE_POLICY) - # 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) - # Import of windows.h breaks min()/max() ADD_DEFINITIONS(-DNOMINMAX) @@ -201,7 +212,14 @@ SET(QT_QTTEST_LIBRARY Qt5::Test) # check for libsndfile FIND_PACKAGE(SndFile REQUIRED) -IF(NOT SNDFILE_FOUND) +IF(SNDFILE_FOUND) + IF(SndFile_VERSION VERSION_GREATER_EQUAL "1.1.0") + SET(LMMS_HAVE_SNDFILE_MP3 TRUE) + ELSE() + MESSAGE("libsndfile version is < 1.1.0; MP3 import disabled") + SET(LMMS_HAVE_SNDFILE_MP3 FALSE) + ENDIF() +ELSE() MESSAGE(FATAL_ERROR "LMMS requires libsndfile1 and libsndfile1-dev >= 1.0.18 - please install, remove CMakeCache.txt and try again!") ENDIF() # check if we can use SFC_SET_COMPRESSION_LEVEL @@ -428,8 +446,6 @@ IF(WANT_MP3LAME) SET(STATUS_MP3LAME "OK") ELSE(LAME_FOUND) SET(STATUS_MP3LAME "not found, please install libmp3lame-dev (or similar)") - SET(LAME_LIBRARIES "") - SET(LAME_INCLUDE_DIRS "") ENDIF(LAME_FOUND) ELSE(WANT_MP3LAME) SET(STATUS_MP3LAME "Disabled for build") @@ -524,7 +540,7 @@ ENDIF() # check for Fluidsynth IF(WANT_SF2) - find_package(FluidSynth 1.1.0) + find_package(FluidSynth 1.1.7) if(FluidSynth_FOUND) SET(LMMS_HAVE_FLUIDSYNTH TRUE) if(FluidSynth_VERSION_STRING VERSION_GREATER_EQUAL 2) diff --git a/cmake/modules/BuildPlugin.cmake b/cmake/modules/BuildPlugin.cmake index f8b3d315392..70e518c93ce 100644 --- a/cmake/modules/BuildPlugin.cmake +++ b/cmake/modules/BuildPlugin.cmake @@ -56,11 +56,7 @@ MACRO(BUILD_PLUGIN PLUGIN_NAME) ADD_LIBRARY(${PLUGIN_NAME} ${PLUGIN_LINK} ${PLUGIN_SOURCES} ${plugin_MOC_out} ${RCC_OUT}) - TARGET_LINK_LIBRARIES(${PLUGIN_NAME} Qt5::Widgets Qt5::Xml) - - IF(LMMS_BUILD_WIN32) - TARGET_LINK_LIBRARIES(${PLUGIN_NAME} lmms) - ENDIF(LMMS_BUILD_WIN32) + target_link_libraries("${PLUGIN_NAME}" lmms Qt5::Widgets Qt5::Xml) INSTALL(TARGETS ${PLUGIN_NAME} LIBRARY DESTINATION "${PLUGIN_DIR}" @@ -70,10 +66,7 @@ MACRO(BUILD_PLUGIN PLUGIN_NAME) IF(LMMS_BUILD_APPLE) IF ("${PLUGIN_LINK}" STREQUAL "SHARED") SET_TARGET_PROPERTIES(${PLUGIN_NAME} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") - ELSE() - SET_TARGET_PROPERTIES(${PLUGIN_NAME} PROPERTIES LINK_FLAGS "-bundle_loader \"${CMAKE_BINARY_DIR}/lmms\"") ENDIF() - ADD_DEPENDENCIES(${PLUGIN_NAME} lmms) ENDIF(LMMS_BUILD_APPLE) IF(LMMS_BUILD_WIN32) add_custom_command( diff --git a/cmake/modules/CompileCache.cmake b/cmake/modules/CompileCache.cmake index ed4622bd921..56486e24ffb 100644 --- a/cmake/modules/CompileCache.cmake +++ b/cmake/modules/CompileCache.cmake @@ -1,25 +1,40 @@ -option(USE_COMPILE_CACHE "Use ccache or clcache for compilation" OFF) +option(USE_COMPILE_CACHE "Use a compiler cache for compilation" OFF) # Compatibility for old option name if(USE_CCACHE) set(USE_COMPILE_CACHE ON) endif() -if(USE_COMPILE_CACHE) - if(MSVC) - set(CACHE_TOOL_NAME clcache) - elseif(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|AppleClang|Clang)") - set(CACHE_TOOL_NAME ccache) - else() - message(WARNING "Compile cache only available with MSVC or GNU") - endif() +if(NOT USE_COMPILE_CACHE) + return() +endif() - find_program(CACHE_TOOL ${CACHE_TOOL_NAME}) - if (CACHE_TOOL) - message(STATUS "Using ${CACHE_TOOL} found for caching") - set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CACHE_TOOL}) - set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CACHE_TOOL}) - else() - message(WARNING "USE_COMPILE_CACHE enabled, but no ${CACHE_TOOL_NAME} found") +if(NOT CMAKE_CXX_COMPILER_ID MATCHES "(GNU|AppleClang|Clang|MSVC)") + message(WARNING "Compiler cache only available with MSVC or GNU") + return() +endif() + +set(CACHE_TOOL_NAME ccache) +find_program(CACHE_TOOL "${CACHE_TOOL_NAME}") +if(NOT CACHE_TOOL) + message(WARNING "USE_COMPILE_CACHE enabled, but no ${CACHE_TOOL_NAME} found") + return() +endif() + +if(MSVC) + # ccache doesn't support debug information in the PDB format. Setting the + # debug information format requires CMP0141, introduced with CMake 3.25, to + # be set to NEW prior to the initial `project` command. + if(CMAKE_VERSION VERSION_LESS "3.25") + message(WARNING "Use of compiler cache with MSVC requires at least CMake 3.25") + return() endif() + + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") endif() + +message(STATUS "Using ${CACHE_TOOL} for compiler caching") + +# TODO CMake 3.21: Use CMAKE___LAUNCHER variables instead +set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CACHE_TOOL}") +set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CACHE_TOOL}") diff --git a/cmake/modules/FindFluidSynth.cmake b/cmake/modules/FindFluidSynth.cmake index fcc00cd7d0e..70c40b8d8fa 100644 --- a/cmake/modules/FindFluidSynth.cmake +++ b/cmake/modules/FindFluidSynth.cmake @@ -34,7 +34,7 @@ if(FluidSynth_INCLUDE_DIR AND FluidSynth_LIBRARY) if(VCPKG_INSTALLED_DIR) include(ImportedTargetHelpers) - _get_vcpkg_library_configs(FluidSynth_IMPLIB_RELEASE FluidSynth_IMPLIB_DEBUG "${FluidSynth_LIBRARY}") + get_vcpkg_library_configs(FluidSynth_IMPLIB_RELEASE FluidSynth_IMPLIB_DEBUG "${FluidSynth_LIBRARY}") else() set(FluidSynth_IMPLIB_RELEASE "${FluidSynth_LIBRARY}") endif() diff --git a/cmake/modules/FindLame.cmake b/cmake/modules/FindLame.cmake index c3fb09c5bf1..3017dc5aa7a 100644 --- a/cmake/modules/FindLame.cmake +++ b/cmake/modules/FindLame.cmake @@ -1,37 +1,31 @@ -# - Try to find LAME -# Once done this will define +# Copyright (c) 2023 Dominic Clark # -# Lame_FOUND - system has liblame -# Lame_INCLUDE_DIRS - the liblame include directory -# Lame_LIBRARIES - The liblame libraries -# mp3lame::mp3lame - an imported target providing lame +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. -find_package(mp3lame CONFIG QUIET) +include(ImportedTargetHelpers) -if(TARGET mp3lame::mp3lame) - # Extract details for find_package_handle_standard_args - get_target_property(Lame_LIBRARIES mp3lame::mp3lame LOCATION) - get_target_property(Lame_INCLUDE_DIRS mp3lame::mp3lame INTERFACE_INCLUDE_DIRECTORIES) -else() - find_path(Lame_INCLUDE_DIRS lame/lame.h) - find_library(Lame_LIBRARIES mp3lame) - - list(APPEND Lame_DEFINITIONS HAVE_LIBMP3LAME=1) - - mark_as_advanced(Lame_INCLUDE_DIRS Lame_LIBRARIES Lame_DEFINITIONS) +find_package_config_mode_with_fallback(mp3lame mp3lame::mp3lame + LIBRARY_NAMES "mp3lame" + INCLUDE_NAMES "lame/lame.h" + PREFIX Lame +) - if(Lame_LIBRARIES AND Lame_INCLUDE_DIRS) - add_library(mp3lame::mp3lame UNKNOWN IMPORTED) +determine_version_from_source(Lame_VERSION mp3lame::mp3lame [[ + #include + #include - set_target_properties(mp3lame::mp3lame PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${Lame_INCLUDE_DIRS}" - INTERFACE_COMPILE_DEFINITIONS "${Lame_DEFINITIONS}" - IMPORTED_LOCATION "${Lame_LIBRARIES}" - ) - endif() -endif() + auto main() -> int + { + auto version = lame_version_t{}; + get_lame_version_numerical(&version); + std::cout << version.major << "." << version.minor; + } +]]) include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Lame - REQUIRED_VARS Lame_LIBRARIES Lame_INCLUDE_DIRS + REQUIRED_VARS Lame_LIBRARY Lame_INCLUDE_DIRS + VERSION_VAR Lame_VERSION ) diff --git a/cmake/modules/FindOggVorbis.cmake b/cmake/modules/FindOggVorbis.cmake index 79a9ab40657..cfbd73256c3 100644 --- a/cmake/modules/FindOggVorbis.cmake +++ b/cmake/modules/FindOggVorbis.cmake @@ -1,86 +1,68 @@ -# - Try to find the OggVorbis libraries -# Once done this will define +# Copyright (c) 2023 Dominic Clark # -# OGGVORBIS_FOUND - system has OggVorbis -# OGGVORBIS_VERSION - set either to 1 or 2 -# OGGVORBIS_INCLUDE_DIR - the OggVorbis include directory -# OGGVORBIS_LIBRARIES - The libraries needed to use OggVorbis -# OGG_LIBRARY - The Ogg library -# VORBIS_LIBRARY - The Vorbis library -# VORBISFILE_LIBRARY - The VorbisFile library -# VORBISENC_LIBRARY - The VorbisEnc library - -# Copyright (c) 2006, Richard Laerkaeng, -# -# Redistribution and use is allowed according to the terms of the BSD license. +# Redistribution and use is allowed according to the terms of the New BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. - -include (CheckLibraryExists) - -find_path(VORBIS_INCLUDE_DIR vorbis/vorbisfile.h) -find_path(OGG_INCLUDE_DIR ogg/ogg.h) - -find_library(OGG_LIBRARY NAMES ogg) -find_library(VORBIS_LIBRARY NAMES vorbis) -find_library(VORBISFILE_LIBRARY NAMES vorbisfile) -find_library(VORBISENC_LIBRARY NAMES vorbisenc) - - -if (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) - set(OGGVORBIS_FOUND TRUE) - - set(OGGVORBIS_LIBRARIES ${OGG_LIBRARY} ${VORBIS_LIBRARY} ${VORBISFILE_LIBRARY} ${VORBISENC_LIBRARY}) - - set(_CMAKE_REQUIRED_LIBRARIES_TMP ${CMAKE_REQUIRED_LIBRARIES}) - set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} ${OGGVORBIS_LIBRARIES}) - check_library_exists(vorbis vorbis_bitrate_addblock "" HAVE_LIBVORBISENC2) - set(CMAKE_REQUIRED_LIBRARIES ${_CMAKE_REQUIRED_LIBRARIES_TMP}) - - if (HAVE_LIBVORBISENC2) - set (OGGVORBIS_VERSION 2) - else (HAVE_LIBVORBISENC2) - set (OGGVORBIS_VERSION 1) - endif (HAVE_LIBVORBISENC2) - -else (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) - set (OGGVORBIS_VERSION) - set(OGGVORBIS_FOUND FALSE) -endif (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) - - -if (OGGVORBIS_FOUND) - if (NOT OggVorbis_FIND_QUIETLY) - message(STATUS "Found OggVorbis: ${OGGVORBIS_LIBRARIES}") - endif (NOT OggVorbis_FIND_QUIETLY) -else (OGGVORBIS_FOUND) - if (OggVorbis_FIND_REQUIRED) - message(FATAL_ERROR "Could NOT find OggVorbis libraries") - endif (OggVorbis_FIND_REQUIRED) - if (NOT OggVorbis_FIND_QUITELY) - message(STATUS "Could NOT find OggVorbis libraries") - endif (NOT OggVorbis_FIND_QUITELY) -endif (OGGVORBIS_FOUND) - -#check_include_files(vorbis/vorbisfile.h HAVE_VORBISFILE_H) -#check_library_exists(ogg ogg_page_version "" HAVE_LIBOGG) -#check_library_exists(vorbis vorbis_info_init "" HAVE_LIBVORBIS) -#check_library_exists(vorbisfile ov_open "" HAVE_LIBVORBISFILE) -#check_library_exists(vorbisenc vorbis_info_clear "" HAVE_LIBVORBISENC) -#check_library_exists(vorbis vorbis_bitrate_addblock "" HAVE_LIBVORBISENC2) - -#if (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) -# message(STATUS "Ogg/Vorbis found") -# set (VORBIS_LIBS "-lvorbis -logg") -# set (VORBISFILE_LIBS "-lvorbisfile") -# set (VORBISENC_LIBS "-lvorbisenc") -# set (OGGVORBIS_FOUND TRUE) -# if (HAVE_LIBVORBISENC2) -# set (HAVE_VORBIS 2) -# else (HAVE_LIBVORBISENC2) -# set (HAVE_VORBIS 1) -# endif (HAVE_LIBVORBISENC2) -#else (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) -# message(STATUS "Ogg/Vorbis not found") -#endif (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) - +include(ImportedTargetHelpers) + +find_package_config_mode_with_fallback(Ogg Ogg::ogg + LIBRARY_NAMES "ogg" + INCLUDE_NAMES "ogg/ogg.h" + PKG_CONFIG ogg +) + +find_package_config_mode_with_fallback(Vorbis Vorbis::vorbis + LIBRARY_NAMES "vorbis" + INCLUDE_NAMES "vorbis/codec.h" + PKG_CONFIG vorbis + DEPENDS Ogg::ogg +) + +find_package_config_mode_with_fallback(Vorbis Vorbis::vorbisfile + LIBRARY_NAMES "vorbisfile" + INCLUDE_NAMES "vorbis/vorbisfile.h" + PKG_CONFIG vorbisfile + DEPENDS Vorbis::vorbis + PREFIX VorbisFile +) + +find_package_config_mode_with_fallback(Vorbis Vorbis::vorbisenc + LIBRARY_NAMES "vorbisenc" + INCLUDE_NAMES "vorbis/vorbisenc.h" + PKG_CONFIG vorbisenc + DEPENDS Vorbis::vorbis + PREFIX VorbisEnc +) + +determine_version_from_source(Vorbis_VERSION Vorbis::vorbis [[ + #include + #include + #include + + auto main() -> int + { + // Version string has the format "org name version" + const auto version = std::string_view{vorbis_version_string()}; + const auto nameBegin = version.find(' ') + 1; + const auto versionBegin = version.find(' ', nameBegin) + 1; + std::cout << version.substr(versionBegin); + } +]]) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(OggVorbis + REQUIRED_VARS + Ogg_LIBRARY + Ogg_INCLUDE_DIRS + Vorbis_LIBRARY + Vorbis_INCLUDE_DIRS + VorbisFile_LIBRARY + VorbisFile_INCLUDE_DIRS + VorbisEnc_LIBRARY + VorbisEnc_INCLUDE_DIRS + # This only reports the Vorbis version - Ogg can have a different version, + # so if we ever care about that, it should be split off into a different + # find module. + VERSION_VAR Vorbis_VERSION +) diff --git a/cmake/modules/FindPortaudio.cmake b/cmake/modules/FindPortaudio.cmake index f9c7699f451..e7cfa1383fc 100644 --- a/cmake/modules/FindPortaudio.cmake +++ b/cmake/modules/FindPortaudio.cmake @@ -1,44 +1,34 @@ -# Copyright (c) 2022 Dominic Clark +# Copyright (c) 2023 Dominic Clark # # Redistribution and use is allowed according to the terms of the New BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. -# Try config mode if possible -find_package(portaudio CONFIG QUIET) +include(ImportedTargetHelpers) -if(TARGET portaudio) - # Extract details for find_package_handle_standard_args - get_target_property(Portaudio_LIBRARY portaudio LOCATION) - get_target_property(Portaudio_INCLUDE_DIR portaudio INTERFACE_INCLUDE_DIRECTORIES) -else() - # Attempt to find PortAudio using PkgConfig, if we have it - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(PORTAUDIO_PKG portaudio-2.0) - endif() - - # Find the library and headers using the results from PkgConfig as a guide - find_library(Portaudio_LIBRARY - NAMES "portaudio" - HINTS ${PORTAUDIO_PKG_LIBRARY_DIRS} - ) +find_package_config_mode_with_fallback(portaudio portaudio + LIBRARY_NAMES "portaudio" + INCLUDE_NAMES "portaudio.h" + PKG_CONFIG portaudio-2.0 + PREFIX Portaudio +) - find_path(Portaudio_INCLUDE_DIR - NAMES "portaudio.h" - HINTS ${PORTAUDIO_PKG_INCLUDE_DIRS} - ) +determine_version_from_source(Portaudio_VERSION portaudio [[ + #include + #include "portaudio.h" - # Create an imported target for PortAudio if we succeeded in finding it. - if(Portaudio_LIBRARY AND Portaudio_INCLUDE_DIR) - add_library(portaudio UNKNOWN IMPORTED) - set_target_properties(portaudio PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${Portaudio_INCLUDE_DIR}" - IMPORTED_LOCATION "${Portaudio_LIBRARY}" - ) - endif() -endif() + auto main() -> int + { + // Version number has the format 0xMMmmpp + const auto version = Pa_GetVersion(); + std::cout << ((version >> 16) & 0xff) + << "." << ((version >> 8) & 0xff) + << "." << ((version >> 0) & 0xff); + } +]]) include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Portaudio - REQUIRED_VARS Portaudio_LIBRARY Portaudio_INCLUDE_DIR + REQUIRED_VARS Portaudio_LIBRARY Portaudio_INCLUDE_DIRS + VERSION_VAR Portaudio_VERSION ) diff --git a/cmake/modules/FindSTK.cmake b/cmake/modules/FindSTK.cmake index 0718f5039a3..5564d24f8fd 100644 --- a/cmake/modules/FindSTK.cmake +++ b/cmake/modules/FindSTK.cmake @@ -1,39 +1,27 @@ -# Try config mode first -find_package(unofficial-libstk CONFIG QUIET) +include(ImportedTargetHelpers) -if(TARGET unofficial::libstk::libstk) - # Extract details for find_package_handle_standard_args - get_target_property(STK_LIBRARY unofficial::libstk::libstk LOCATION) - get_target_property(STK_INCLUDE_DIR unofficial::libstk::libstk INTERFACE_INCLUDE_DIRECTORIES) -else() - find_path(STK_INCLUDE_DIR - NAMES stk/Stk.h - PATH /usr/include /usr/local/include "${CMAKE_INSTALL_PREFIX}/include" "${CMAKE_FIND_ROOT_PATH}/include" - ) +# TODO CMake 3.18: Alias this target to something less hideous +find_package_config_mode_with_fallback(unofficial-libstk unofficial::libstk::libstk + LIBRARY_NAMES "stk" + INCLUDE_NAMES "stk/Stk.h" + LIBRARY_HINTS "/usr/lib" "/usr/local/lib" "${CMAKE_INSTALL_PREFIX}/lib" "${CMAKE_FIND_ROOT_PATH}/lib" + INCLUDE_HINTS "/usr/include" "/usr/local/include" "${CMAKE_INSTALL_PREFIX}/include" "${CMAKE_FIND_ROOT_PATH}/include" + PREFIX STK +) - find_library(STK_LIBRARY - NAMES stk - PATH /usr/lib /usr/local/lib "${CMAKE_INSTALL_PREFIX}/lib" "${CMAKE_FIND_ROOT_PATH}/lib" +# Find STK rawwave path +if(STK_INCLUDE_DIRS) + list(GET STK_INCLUDE_DIRS 0 STK_INCLUDE_DIR) + find_path(STK_RAWWAVE_ROOT + NAMES silence.raw sinewave.raw + HINTS "${STK_INCLUDE_DIR}/.." + PATH_SUFFIXES share/stk/rawwaves share/libstk/rawwaves ) - - if(STK_INCLUDE_DIR AND STK_LIBRARY) - # Yes, this target name is hideous, but it matches that provided by vcpkg - add_library(unofficial::libstk::libstk UNKNOWN IMPORTED) - set_target_properties(unofficial::libstk::libstk PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${STK_INCLUDE_DIR}" - IMPORTED_LOCATION "${STK_LIBRARY}" - ) - endif() endif() -# find STK rawwave path -find_path(STK_RAWWAVE_ROOT - NAMES silence.raw sinewave.raw - HINTS "${STK_INCLUDE_DIR}/.." - PATH_SUFFIXES share/stk/rawwaves share/libstk/rawwaves -) - include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(STK REQUIRED_VARS STK_LIBRARY STK_INCLUDE_DIR + # STK doesn't appear to expose its version, so we can't pass it here ) diff --git a/cmake/modules/FindSamplerate.cmake b/cmake/modules/FindSamplerate.cmake index 53b69f6c722..683748c59f0 100644 --- a/cmake/modules/FindSamplerate.cmake +++ b/cmake/modules/FindSamplerate.cmake @@ -1,34 +1,35 @@ -# FindFFTW.cmake - Try to find FFTW3 -# Copyright (c) 2018 Lukas W -# This file is MIT licensed. -# See http://opensource.org/licenses/MIT +# Copyright (c) 2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. -find_package(PkgConfig QUIET) -if(PKG_CONFIG_FOUND) - pkg_check_modules(SAMPLERATE_PKG samplerate) -endif() +include(ImportedTargetHelpers) -find_path(SAMPLERATE_INCLUDE_DIR - NAMES samplerate.h - PATHS ${SAMPLERATE_PKG_INCLUDE_DIRS} +find_package_config_mode_with_fallback(SampleRate SampleRate::samplerate + LIBRARY_NAMES "samplerate" "libsamplerate" "libsamplerate-0" + INCLUDE_NAMES "samplerate.h" + PKG_CONFIG samplerate + PREFIX Samplerate ) -set(SAMPLERATE_NAMES samplerate libsamplerate) -if(Samplerate_FIND_VERSION_MAJOR) - list(APPEND SAMPLERATE_NAMES libsamplerate-${Samplerate_FIND_VERSION_MAJOR}) -else() - list(APPEND SAMPLERATE_NAMES libsamplerate-0) -endif() +determine_version_from_source(Samplerate_VERSION SampleRate::samplerate [[ + #include + #include + #include -find_library(SAMPLERATE_LIBRARY - NAMES ${SAMPLERATE_NAMES} - PATHS ${SAMPLERATE_PKG_LIBRARY_DIRS} -) + auto main() -> int + { + // Version string has the format "name-version copyright" + const auto version = std::string_view{src_get_version()}; + const auto begin = version.find('-') + 1; + const auto end = version.find(' ', begin); + std::cout << version.substr(begin, end - begin); + } +]]) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(SAMPLERATE DEFAULT_MSG SAMPLERATE_LIBRARY SAMPLERATE_INCLUDE_DIR) - -mark_as_advanced(SAMPLERATE_INCLUDE_DIR SAMPLERATE_LIBRARY ) -set(SAMPLERATE_LIBRARIES ${SAMPLERATE_LIBRARY} ) -set(SAMPLERATE_INCLUDE_DIRS ${SAMPLERATE_INCLUDE_DIR}) +find_package_handle_standard_args(Samplerate + REQUIRED_VARS Samplerate_LIBRARY Samplerate_INCLUDE_DIRS + VERSION_VAR Samplerate_VERSION +) diff --git a/cmake/modules/FindSndFile.cmake b/cmake/modules/FindSndFile.cmake index 28ebb7bb73f..d69fa6331ce 100644 --- a/cmake/modules/FindSndFile.cmake +++ b/cmake/modules/FindSndFile.cmake @@ -1,39 +1,34 @@ -# FindSndFile.cmake - Try to find libsndfile -# Copyright (c) 2018 Lukas W -# This file is MIT licensed. -# See http://opensource.org/licenses/MIT +# Copyright (c) 2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. -# Try pkgconfig for hints -find_package(PkgConfig QUIET) -if(PKG_CONFIG_FOUND) - pkg_check_modules(SNDFILE_PKG sndfile) -endif(PKG_CONFIG_FOUND) -set(SndFile_DEFINITIONS ${SNDFILE_PKG_CFLAGS_OTHER}) +include(ImportedTargetHelpers) -if(WIN32) - # Try Vcpkg - find_package(LibSndFile ${SndFile_FIND_VERSION} CONFIG QUIET) - if(LibSndFile_FOUND) - get_target_property(LibSndFile_Location sndfile-shared LOCATION) - get_target_property(LibSndFile_Include_Path sndfile-shared INTERFACE_INCLUDE_DIRECTORIES) - get_filename_component(LibSndFile_Path LibSndFile_Location PATH) - endif() -endif() - -find_path(SNDFILE_INCLUDE_DIR - NAMES sndfile.h - PATHS ${SNDFILE_PKG_INCLUDE_DIRS} ${LibSndFile_Include_Path} +find_package_config_mode_with_fallback(SndFile SndFile::sndfile + LIBRARY_NAMES "sndfile" "libsndfile" "libsndfile-1" + INCLUDE_NAMES "sndfile.h" + PKG_CONFIG sndfile ) -find_library(SNDFILE_LIBRARY - NAMES sndfile libsndfile libsndfile-1 - PATHS ${SNDFILE_PKG_LIBRARY_DIRS} ${LibSndFile_Path} -) +determine_version_from_source(SndFile_VERSION SndFile::sndfile [[ + #include + #include + #include -find_package(PackageHandleStandardArgs) -find_package_handle_standard_args(SndFile DEFAULT_MSG SNDFILE_LIBRARY SNDFILE_INCLUDE_DIR) + auto main() -> int + { + // Version string has the format "name-version", optionally followed by "-exp" + const auto version = std::string_view{sf_version_string()}; + const auto begin = version.find('-') + 1; + const auto end = version.find('-', begin); + std::cout << version.substr(begin, end - begin); + } +]]) -set(SNDFILE_LIBRARIES ${SNDFILE_LIBRARY}) -set(SNDFILE_INCLUDE_DIRS ${SNDFILE_INCLUDE_DIR}) +include(FindPackageHandleStandardArgs) -mark_as_advanced(SNDFILE_LIBRARY SNDFILE_LIBRARIES SNDFILE_INCLUDE_DIR SNDFILE_INCLUDE_DIRS) +find_package_handle_standard_args(SndFile + REQUIRED_VARS SndFile_LIBRARY SndFile_INCLUDE_DIRS + VERSION_VAR SndFile_VERSION +) diff --git a/cmake/modules/ImportedTargetHelpers.cmake b/cmake/modules/ImportedTargetHelpers.cmake index 87b3aeedc33..d3d979901e9 100644 --- a/cmake/modules/ImportedTargetHelpers.cmake +++ b/cmake/modules/ImportedTargetHelpers.cmake @@ -1,7 +1,178 @@ +# ImportedTargetHelpers.cmake - various helper functions for use in find modules. +# +# Copyright (c) 2022-2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# If the version variable is not yet set, build the source linked to the target, +# run it, and set the version variable to the output. Useful for libraries which +# do not expose the version information in a header where it can be extracted +# with regular expressions, but do provide a function to get the version. +# +# Usage: +# determine_version_from_source( +# # The cache variable in which to store the computed version +# # The target which the source will link to +# # The source code to determine the version +# ) +function(determine_version_from_source _version_out _target _source) + # Return if we already know the version, or the target was not found + if(NOT "${${_version_out}}" STREQUAL "" OR NOT TARGET "${_target}") + return() + endif() + + # Return with a notice if cross-compiling, since we are unlikely to be able + # to run the compiled source + if(CMAKE_CROSSCOMPILING) + message( + "${_target} was found but the version could not be determined automatically.\n" + "Set the cache variable `${_version_out}` to the version you have installed." + ) + return() + endif() + + # Write the source code to a temporary file + string(SHA1 _source_hash "${_source}") + set(_source_file "${CMAKE_CURRENT_BINARY_DIR}/${_source_hash}.cpp") + file(WRITE "${_source_file}" "${_source}") + + # Build and run the temporary file to get the version + # TODO CMake 3.25: Use the new signature for try_run which has a NO_CACHE + # option and doesn't require separate file management. + try_run( + _dvfs_run_result _dvfs_compile_result "${CMAKE_CURRENT_BINARY_DIR}" + SOURCES "${_source_file}" + LINK_LIBRARIES "${_target}" + CXX_STANDARD 17 + RUN_OUTPUT_VARIABLE _run_output + COMPILE_OUTPUT_VARIABLE _compile_output + ) + + # Clean up the temporary file + file(REMOVE "${_source_file}") + + # Set the version if the run was successful, using a cache variable since + # this version check may be relatively expensive. Otherwise, log the error + # and inform the user. + if(_dvfs_run_result EQUAL "0") + set("${_version_out}" "${_run_output}" CACHE INTERNAL "Version of ${_target}") + else() + message(DEBUG "${_compile_output}") + message( + "${_target} was found but the version could not be determined automatically.\n" + "Set the cache variable `${_version_out}` to the version you have installed." + ) + endif() +endfunction() + +# Search for a package using config mode. If this fails to find the desired +# target, use the specified fallbacks and add the target if they succeed. Set +# the variables `prefix_LIBRARY`, `prefix_INCLUDE_DIRS`, and `prefix_VERSION` +# if found for the caller to pass to `find_package_handle_standard_args`. +# +# Usage: +# find_package_config_mode_with_fallback( +# # The package to search for with config mode +# # The target to expect from config mode, or define if not found +# LIBRARY_NAMES names... # Possible library names to search for as a fallback +# INCLUDE_NAMES names... # Possible header names to search for as a fallback +# [PKG_CONFIG ] # The pkg-config name to search for as a fallback +# [LIBRARY_HINTS hints...] # Locations to look for libraries +# [INCLUDE_HINTS hints...] # Locations to look for headers +# [DEPENDS dependencies...] # Dependencies of the target - added to INTERFACE_LINK_LIBRARIES, and will fail if not found +# [PREFIX ] # The prefix for result variables - defaults to the package name +# ) +function(find_package_config_mode_with_fallback _fpcmwf_PACKAGE_NAME _fpcmwf_TARGET_NAME) + # Parse remaining arguments + set(_options "") + set(_one_value_args "PKG_CONFIG" "PREFIX") + set(_multi_value_args "LIBRARY_NAMES" "LIBRARY_HINTS" "INCLUDE_NAMES" "INCLUDE_HINTS" "DEPENDS") + cmake_parse_arguments(PARSE_ARGV 2 _fpcmwf "${_options}" "${_one_value_args}" "${_multi_value_args}") + + # Compute result variable names + if(NOT DEFINED _fpcmwf_PREFIX) + set(_fpcmwf_PREFIX "${_fpcmwf_PACKAGE_NAME}") + endif() + set(_version_var "${_fpcmwf_PREFIX}_VERSION") + set(_library_var "${_fpcmwf_PREFIX}_LIBRARY") + set(_include_var "${_fpcmwf_PREFIX}_INCLUDE_DIRS") + + # Try config mode if possible + find_package("${_fpcmwf_PACKAGE_NAME}" CONFIG QUIET) + + if(TARGET "${_fpcmwf_TARGET_NAME}") + # Extract package details from existing target + get_target_property("${_library_var}" "${_fpcmwf_TARGET_NAME}" LOCATION) + get_target_property("${_include_var}" "${_fpcmwf_TARGET_NAME}" INTERFACE_INCLUDE_DIRECTORIES) + if(DEFINED "${_fpcmwf_PACKAGE_NAME}_VERSION") + set("${_version_var}" "${${_fpcmwf_PACKAGE_NAME}_VERSION}") + endif() + else() + # Check whether the dependencies exist + foreach(_dependency IN LISTS _fpcmwf_DEPENDS) + if(NOT TARGET "${_dependency}") + return() + endif() + endforeach() + + # Attempt to find the package using pkg-config, if we have it and it was requested + set(_pkg_config_prefix "${_fpcmwf_PKG_CONFIG}_PKG") + if(DEFINED _fpcmwf_PKG_CONFIG) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules("${_pkg_config_prefix}" QUIET "${_fpcmwf_PKG_CONFIG}") + if("${${_pkg_config_prefix}_FOUND}") + set("${_version_var}" "${${_pkg_config_prefix}_VERSION}") + endif() + endif() + endif() + + # Find the library and headers using the results from pkg-config as a guide + find_library("${_library_var}" + NAMES ${_fpcmwf_LIBRARY_NAMES} + HINTS ${${_pkg_config_prefix}_LIBRARY_DIRS} ${_fpcmwf_LIBRARY_HINTS} + ) + + find_path("${_include_var}" + NAMES ${_fpcmwf_INCLUDE_NAMES} + HINTS ${${_pkg_config_prefix}_INCLUDE_DIRS} ${_fpcmwf_INCLUDE_HINTS} + ) + + # Create an imported target if we succeeded in finding the package + if(${_library_var} AND ${_include_var}) + add_library("${_fpcmwf_TARGET_NAME}" UNKNOWN IMPORTED) + set_target_properties("${_fpcmwf_TARGET_NAME}" PROPERTIES + IMPORTED_LOCATION "${${_library_var}}" + INTERFACE_INCLUDE_DIRECTORIES "${${_include_var}}" + INTERFACE_LINK_LIBRARIES "${_fpcmwf_DEPENDS}" + ) + endif() + + mark_as_advanced("${_library_var}" "${_include_var}") + endif() + + # Return results to caller + if(DEFINED "${_version_var}") + set("${_version_var}" "${${_version_var}}" PARENT_SCOPE) + else() + unset("${_version_var}" PARENT_SCOPE) + endif() + set("${_library_var}" "${${_library_var}}" PARENT_SCOPE) + set("${_include_var}" "${${_include_var}}" PARENT_SCOPE) +endfunction() + # Given a library in vcpkg, find appropriate debug and release versions. If only -# one version exists, it is used as the release version, and the debug version -# is not set. -function(_get_vcpkg_library_configs _release_out _debug_out _library) +# one version exists, use it as the release version, and do not set the debug +# version. +# +# Usage: +# get_vcpkg_library_configs( +# # Variable in which to store the path to the release version of the library +# # Variable in which to store the path to the debug version of the library +# # Known path to some version of the library +# ) +function(get_vcpkg_library_configs _release_out _debug_out _library) # We want to do all operations within the vcpkg directory file(RELATIVE_PATH _lib_relative "${VCPKG_INSTALLED_DIR}" "${_library}") diff --git a/cmake/modules/InstallDependencies.cmake b/cmake/modules/InstallDependencies.cmake index 167a93f351f..29e5b207c18 100644 --- a/cmake/modules/InstallDependencies.cmake +++ b/cmake/modules/InstallDependencies.cmake @@ -1,7 +1,8 @@ include(GetPrerequisites) include(CMakeParseArguments) -# Project's cmake_minimum_required doesn't always propagate +# Project's cmake_minimum_required doesn't propagate to install scripts +cmake_policy(PUSH) cmake_policy(SET CMP0057 NEW) # Support new if() IN_LIST operator. function(make_absolute var) @@ -182,3 +183,5 @@ function(FIND_PREREQUISITES target RESULT_VAR exclude_system recurse set(${RESULT_VAR} ${RESULTS} PARENT_SCOPE) endfunction() + +cmake_policy(POP) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 0a4686fb2c6..8c444aca291 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -41,6 +41,7 @@ SET(LMMS_PLUGIN_LIST HydrogenImport LadspaBrowser LadspaEffect + LOMM Lv2Effect Lv2Instrument Lb302 @@ -59,6 +60,7 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SlicerT SpectrumAnalyzer StereoEnhancer StereoMatrix diff --git a/data/samples/bassloops/briff01.ogg b/data/samples/bassloops/briff01 - 140 BPM.ogg similarity index 98% rename from data/samples/bassloops/briff01.ogg rename to data/samples/bassloops/briff01 - 140 BPM.ogg index a307df85f61..0b9cc32f7f8 100644 Binary files a/data/samples/bassloops/briff01.ogg and b/data/samples/bassloops/briff01 - 140 BPM.ogg differ diff --git a/data/samples/bassloops/rave_bass01.ogg b/data/samples/bassloops/rave_bass01 - 180 BPM.ogg similarity index 98% rename from data/samples/bassloops/rave_bass01.ogg rename to data/samples/bassloops/rave_bass01 - 180 BPM.ogg index 920ff4a740f..335195747d6 100644 Binary files a/data/samples/bassloops/rave_bass01.ogg and b/data/samples/bassloops/rave_bass01 - 180 BPM.ogg differ diff --git a/data/samples/bassloops/rave_bass02.ogg b/data/samples/bassloops/rave_bass02 - 180 BPM.ogg similarity index 98% rename from data/samples/bassloops/rave_bass02.ogg rename to data/samples/bassloops/rave_bass02 - 180 BPM.ogg index ff38123df91..230d99d2e95 100644 Binary files a/data/samples/bassloops/rave_bass02.ogg and b/data/samples/bassloops/rave_bass02 - 180 BPM.ogg differ diff --git a/data/samples/bassloops/tb303_01.ogg b/data/samples/bassloops/tb303_01 - 123 BPM.ogg similarity index 98% rename from data/samples/bassloops/tb303_01.ogg rename to data/samples/bassloops/tb303_01 - 123 BPM.ogg index 41e1b1fc478..1057201756e 100644 Binary files a/data/samples/bassloops/tb303_01.ogg and b/data/samples/bassloops/tb303_01 - 123 BPM.ogg differ diff --git a/data/samples/bassloops/techno_bass01.ogg b/data/samples/bassloops/techno_bass01 - 140 BPM.ogg similarity index 92% rename from data/samples/bassloops/techno_bass01.ogg rename to data/samples/bassloops/techno_bass01 - 140 BPM.ogg index 15c2c8e321c..55cd204ddfb 100644 Binary files a/data/samples/bassloops/techno_bass01.ogg and b/data/samples/bassloops/techno_bass01 - 140 BPM.ogg differ diff --git a/data/samples/bassloops/techno_bass02.ogg b/data/samples/bassloops/techno_bass02 - 140 BPM.ogg similarity index 93% rename from data/samples/bassloops/techno_bass02.ogg rename to data/samples/bassloops/techno_bass02 - 140 BPM.ogg index 08691f43521..c1f3e8637a1 100644 Binary files a/data/samples/bassloops/techno_bass02.ogg and b/data/samples/bassloops/techno_bass02 - 140 BPM.ogg differ diff --git a/data/samples/bassloops/techno_synth01.ogg b/data/samples/bassloops/techno_synth01 - 140 BPM.ogg similarity index 86% rename from data/samples/bassloops/techno_synth01.ogg rename to data/samples/bassloops/techno_synth01 - 140 BPM.ogg index 95c3d96ae92..1086bb0c89f 100644 Binary files a/data/samples/bassloops/techno_synth01.ogg and b/data/samples/bassloops/techno_synth01 - 140 BPM.ogg differ diff --git a/data/samples/bassloops/techno_synth02.ogg b/data/samples/bassloops/techno_synth02 - 140 BPM.ogg similarity index 87% rename from data/samples/bassloops/techno_synth02.ogg rename to data/samples/bassloops/techno_synth02 - 140 BPM.ogg index dfa972e1def..afb27172b0d 100644 Binary files a/data/samples/bassloops/techno_synth02.ogg and b/data/samples/bassloops/techno_synth02 - 140 BPM.ogg differ diff --git a/data/samples/bassloops/techno_synth03.ogg b/data/samples/bassloops/techno_synth03 - 130 BPM.ogg similarity index 92% rename from data/samples/bassloops/techno_synth03.ogg rename to data/samples/bassloops/techno_synth03 - 130 BPM.ogg index c27cda65317..bffff951757 100644 Binary files a/data/samples/bassloops/techno_synth03.ogg and b/data/samples/bassloops/techno_synth03 - 130 BPM.ogg differ diff --git a/data/samples/bassloops/techno_synth04.ogg b/data/samples/bassloops/techno_synth04 - 140 BPM.ogg similarity index 99% rename from data/samples/bassloops/techno_synth04.ogg rename to data/samples/bassloops/techno_synth04 - 140 BPM.ogg index 0e9a3739833..9dd34f33add 100644 Binary files a/data/samples/bassloops/techno_synth04.ogg and b/data/samples/bassloops/techno_synth04 - 140 BPM.ogg differ diff --git a/data/samples/beats/909beat01.ogg b/data/samples/beats/909beat01 - 122 BPM.ogg similarity index 99% rename from data/samples/beats/909beat01.ogg rename to data/samples/beats/909beat01 - 122 BPM.ogg index 1892eb91bc4..2bae5135716 100644 Binary files a/data/samples/beats/909beat01.ogg and b/data/samples/beats/909beat01 - 122 BPM.ogg differ diff --git a/data/samples/beats/break01.ogg b/data/samples/beats/break01 - 168 BPM.ogg similarity index 94% rename from data/samples/beats/break01.ogg rename to data/samples/beats/break01 - 168 BPM.ogg index d1f5769bd5a..5d9bd2f4bb1 100644 Binary files a/data/samples/beats/break01.ogg and b/data/samples/beats/break01 - 168 BPM.ogg differ diff --git a/data/samples/beats/break02.ogg b/data/samples/beats/break02 - 141 BPM.ogg similarity index 89% rename from data/samples/beats/break02.ogg rename to data/samples/beats/break02 - 141 BPM.ogg index 17243cd9ecc..653662c7571 100644 Binary files a/data/samples/beats/break02.ogg and b/data/samples/beats/break02 - 141 BPM.ogg differ diff --git a/data/samples/beats/break03.ogg b/data/samples/beats/break03 - 168 BPM.ogg similarity index 95% rename from data/samples/beats/break03.ogg rename to data/samples/beats/break03 - 168 BPM.ogg index f806be70ae9..3b3a4b34609 100644 Binary files a/data/samples/beats/break03.ogg and b/data/samples/beats/break03 - 168 BPM.ogg differ diff --git a/data/samples/beats/electro_beat01.ogg b/data/samples/beats/electro_beat01 - 120 BPM.ogg similarity index 99% rename from data/samples/beats/electro_beat01.ogg rename to data/samples/beats/electro_beat01 - 120 BPM.ogg index 57cd690fc96..29352b68375 100644 Binary files a/data/samples/beats/electro_beat01.ogg and b/data/samples/beats/electro_beat01 - 120 BPM.ogg differ diff --git a/data/samples/beats/electro_beat02.ogg b/data/samples/beats/electro_beat02 - 119 BPM.ogg similarity index 98% rename from data/samples/beats/electro_beat02.ogg rename to data/samples/beats/electro_beat02 - 119 BPM.ogg index b89260bab52..775b64d8851 100644 Binary files a/data/samples/beats/electro_beat02.ogg and b/data/samples/beats/electro_beat02 - 119 BPM.ogg differ diff --git a/data/samples/beats/house_loop01.ogg b/data/samples/beats/house_loop01 - 142 BPM.ogg similarity index 98% rename from data/samples/beats/house_loop01.ogg rename to data/samples/beats/house_loop01 - 142 BPM.ogg index 09f3a260bc1..9f04d1debf7 100644 Binary files a/data/samples/beats/house_loop01.ogg and b/data/samples/beats/house_loop01 - 142 BPM.ogg differ diff --git a/data/samples/beats/jungle01.ogg b/data/samples/beats/jungle01 - 168 BPM.ogg similarity index 98% rename from data/samples/beats/jungle01.ogg rename to data/samples/beats/jungle01 - 168 BPM.ogg index 9662e4514ab..b7196044fd3 100644 Binary files a/data/samples/beats/jungle01.ogg and b/data/samples/beats/jungle01 - 168 BPM.ogg differ diff --git a/data/samples/beats/rave_hihat01.ogg b/data/samples/beats/rave_hihat01 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_hihat01.ogg rename to data/samples/beats/rave_hihat01 - 180 BPM.ogg index 236f447a8bf..632bb0becfc 100644 Binary files a/data/samples/beats/rave_hihat01.ogg and b/data/samples/beats/rave_hihat01 - 180 BPM.ogg differ diff --git a/data/samples/beats/rave_hihat02.ogg b/data/samples/beats/rave_hihat02 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_hihat02.ogg rename to data/samples/beats/rave_hihat02 - 180 BPM.ogg index 33329bd559d..a6e5426f4df 100644 Binary files a/data/samples/beats/rave_hihat02.ogg and b/data/samples/beats/rave_hihat02 - 180 BPM.ogg differ diff --git a/data/samples/beats/rave_kick01.ogg b/data/samples/beats/rave_kick01 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_kick01.ogg rename to data/samples/beats/rave_kick01 - 180 BPM.ogg index 79f99ffb89c..5633f6e1c91 100644 Binary files a/data/samples/beats/rave_kick01.ogg and b/data/samples/beats/rave_kick01 - 180 BPM.ogg differ diff --git a/data/samples/beats/rave_kick02.ogg b/data/samples/beats/rave_kick02 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_kick02.ogg rename to data/samples/beats/rave_kick02 - 180 BPM.ogg index 46311286921..c57cfc0d9dd 100644 Binary files a/data/samples/beats/rave_kick02.ogg and b/data/samples/beats/rave_kick02 - 180 BPM.ogg differ diff --git a/data/samples/beats/rave_snare01.ogg b/data/samples/beats/rave_snare01 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_snare01.ogg rename to data/samples/beats/rave_snare01 - 180 BPM.ogg index ceec2d6e0a4..6e17a6af37a 100644 Binary files a/data/samples/beats/rave_snare01.ogg and b/data/samples/beats/rave_snare01 - 180 BPM.ogg differ diff --git a/data/samples/latin/latin_brass01.ogg b/data/samples/latin/latin_brass01 - 140 BPM.ogg similarity index 95% rename from data/samples/latin/latin_brass01.ogg rename to data/samples/latin/latin_brass01 - 140 BPM.ogg index 3bf7dcd27d0..ac9a2c59b25 100644 Binary files a/data/samples/latin/latin_brass01.ogg and b/data/samples/latin/latin_brass01 - 140 BPM.ogg differ diff --git a/data/samples/latin/latin_guitar01.ogg b/data/samples/latin/latin_guitar01 - 126 BPM.ogg similarity index 95% rename from data/samples/latin/latin_guitar01.ogg rename to data/samples/latin/latin_guitar01 - 126 BPM.ogg index 25685013d2c..3c316ec3b19 100644 Binary files a/data/samples/latin/latin_guitar01.ogg and b/data/samples/latin/latin_guitar01 - 126 BPM.ogg differ diff --git a/data/samples/latin/latin_guitar02.ogg b/data/samples/latin/latin_guitar02 - 140 BPM.ogg similarity index 92% rename from data/samples/latin/latin_guitar02.ogg rename to data/samples/latin/latin_guitar02 - 140 BPM.ogg index 3fe4269a215..55b218d98df 100644 Binary files a/data/samples/latin/latin_guitar02.ogg and b/data/samples/latin/latin_guitar02 - 140 BPM.ogg differ diff --git a/data/samples/latin/latin_guitar03.ogg b/data/samples/latin/latin_guitar03 - 120 BPM.ogg similarity index 90% rename from data/samples/latin/latin_guitar03.ogg rename to data/samples/latin/latin_guitar03 - 120 BPM.ogg index 0ae9bb3f78d..1f6a9275ea2 100644 Binary files a/data/samples/latin/latin_guitar03.ogg and b/data/samples/latin/latin_guitar03 - 120 BPM.ogg differ diff --git a/data/themes/classic/automation_ghost_note.png b/data/themes/classic/automation_ghost_note.png new file mode 100644 index 00000000000..d14c047d7ae Binary files /dev/null and b/data/themes/classic/automation_ghost_note.png differ diff --git a/data/themes/classic/edit_tangent.png b/data/themes/classic/edit_tangent.png new file mode 100644 index 00000000000..438673b33a9 Binary files /dev/null and b/data/themes/classic/edit_tangent.png differ diff --git a/data/themes/classic/lcd_11green.png b/data/themes/classic/lcd_11green.png new file mode 100644 index 00000000000..32e923fe887 Binary files /dev/null and b/data/themes/classic/lcd_11green.png differ diff --git a/data/themes/classic/lcd_11green_dot.png b/data/themes/classic/lcd_11green_dot.png new file mode 100644 index 00000000000..9f5a660d33f Binary files /dev/null and b/data/themes/classic/lcd_11green_dot.png differ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index c73da5a2b58..505ab348340 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -8,7 +8,7 @@ QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar { } QMdiArea { - background-image: url(resources:background_artwork.png); + background-image: url("resources:background_artwork.png"); } lmms--gui--Knob { @@ -22,6 +22,7 @@ lmms--gui--AutomationEditor { qproperty-backgroundShade: rgba(255, 255, 255, 15); qproperty-nodeInValueColor: rgba(255, 119, 175, 150); qproperty-nodeOutValueColor: rgba(129, 231, 181, 150); + qproperty-nodeTangentLineColor: rgba(200, 200, 200, 255); qproperty-crossColor: rgb( 255, 51, 51 ); /* Grid colors */ qproperty-lineColor: rgba(128, 128, 128, 80); @@ -32,6 +33,10 @@ lmms--gui--AutomationEditor { qproperty-scaleColor: qlineargradient(spread:reflect, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 #333, stop:1 #202020); + + qproperty-ghostNoteColor: rgba(248, 248, 255, 125); + qproperty-detuningNoteColor: rgba(248, 11, 11, 125); + qproperty-ghostSampleColor: rgba(125, 125, 125, 125); } /* text box */ @@ -142,6 +147,7 @@ lmms--gui--PianoRoll { qproperty-backgroundShade: rgba( 255, 255, 255, 10 ); qproperty-noteModeColor: rgb( 255, 255, 255 ); qproperty-noteColor: rgb( 119, 199, 216 ); + qproperty-stepNoteColor: #9b1313; qproperty-noteTextColor: rgb( 255, 255, 255 ); qproperty-noteOpacity: 128; qproperty-noteBorders: true; /* boolean property, set false to have borderless notes */ @@ -204,7 +210,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; - background: url(resources:cpuload_bg.png); + background: url("resources:cpuload_bg.png"); qproperty-stepSize: 4; } @@ -321,14 +327,14 @@ QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { height: 5px; } -QScrollBar::left-arrow:horizontal { background-image: url(resources:sbarrow_left.png);} -QScrollBar::right-arrow:horizontal { background-image: url(resources:sbarrow_right.png);} -QScrollBar::up-arrow:vertical { background-image: url(resources:sbarrow_up.png);} -QScrollBar::down-arrow:vertical { background-image: url(resources:sbarrow_down.png);} -QScrollBar::left-arrow:horizontal:disabled { background-image: url(resources:sbarrow_left_d.png);} -QScrollBar::right-arrow:horizontal:disabled { background-image: url(resources:sbarrow_right_d.png);} -QScrollBar::up-arrow:vertical:disabled { background-image: url(resources:sbarrow_up_d.png);} -QScrollBar::down-arrow:vertical:disabled { background-image: url(resources:sbarrow_down_d.png);} +QScrollBar::left-arrow:horizontal { background-image: url("resources:sbarrow_left.png");} +QScrollBar::right-arrow:horizontal { background-image: url("resources:sbarrow_right.png");} +QScrollBar::up-arrow:vertical { background-image: url("resources:sbarrow_up.png");} +QScrollBar::down-arrow:vertical { background-image: url("resources:sbarrow_down.png");} +QScrollBar::left-arrow:horizontal:disabled { background-image: url("resources:sbarrow_left_d.png");} +QScrollBar::right-arrow:horizontal:disabled { background-image: url("resources:sbarrow_right_d.png");} +QScrollBar::up-arrow:vertical:disabled { background-image: url("resources:sbarrow_up_d.png");} +QScrollBar::down-arrow:vertical:disabled { background-image: url("resources:sbarrow_down_d.png");} /* background for song editor and pattern editor */ @@ -366,7 +372,7 @@ lmms--gui--TrackOperationsWidget > QPushButton { } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { - image: url(resources:trackop.png); + image: url("resources:trackop.png"); subcontrol-origin: padding; subcontrol-position: center; position: relative; @@ -374,12 +380,12 @@ lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:hover { - image: url(resources:trackop_h.png); + image: url("resources:trackop_h.png"); } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:pressed, lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:checked { - image: url(resources:trackop_c.png); + image: url("resources:trackop_c.png"); position: relative; top: 2px; } @@ -408,7 +414,7 @@ lmms--gui--AutomatableSlider::groove:vertical { lmms--gui--AutomatableSlider::handle:vertical { background: none; - border-image: url(resources:main_slider.png); + border-image: url("resources:main_slider.png"); width: 26px; height: 10px; border-radius: 2px; @@ -427,7 +433,7 @@ lmms--gui--AutomatableSlider::groove:horizontal { lmms--gui--AutomatableSlider::handle:horizontal { background: none; - border-image: url(resources:horizontal_slider.png); + border-image: url("resources:horizontal_slider.png"); width: 10px; height: 26px; border-radius: 2px; @@ -898,6 +904,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--SlicerTView lmms--gui--Knob { + color: rgb(162, 128, 226); + qproperty-outerColor: rgb( 162, 128, 226 ); + qproperty-innerRadius: 1; + qproperty-outerRadius: 11; + qproperty-lineWidth: 3; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; @@ -974,6 +988,11 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--BarModelEditor { + qproperty-backgroundBrush: rgba(28, 73, 51, 255); + qproperty-barBrush: rgba(17, 136, 71, 255); +} + /* palette information */ lmms--gui--LmmsPalette { diff --git a/data/themes/default/automation_ghost_note.png b/data/themes/default/automation_ghost_note.png new file mode 100644 index 00000000000..d14c047d7ae Binary files /dev/null and b/data/themes/default/automation_ghost_note.png differ diff --git a/data/themes/default/edit_tangent.png b/data/themes/default/edit_tangent.png new file mode 100644 index 00000000000..7bc4000947d Binary files /dev/null and b/data/themes/default/edit_tangent.png differ diff --git a/data/themes/default/lcd_11green.png b/data/themes/default/lcd_11green.png new file mode 100644 index 00000000000..32e923fe887 Binary files /dev/null and b/data/themes/default/lcd_11green.png differ diff --git a/data/themes/default/lcd_11green_dot.png b/data/themes/default/lcd_11green_dot.png new file mode 100644 index 00000000000..9f5a660d33f Binary files /dev/null and b/data/themes/default/lcd_11green_dot.png differ diff --git a/data/themes/default/lcd_19purple.png b/data/themes/default/lcd_19purple.png new file mode 100644 index 00000000000..35ecc4ace0e Binary files /dev/null and b/data/themes/default/lcd_19purple.png differ diff --git a/data/themes/default/lcd_19purple_dot.png b/data/themes/default/lcd_19purple_dot.png new file mode 100644 index 00000000000..6582d8c201c Binary files /dev/null and b/data/themes/default/lcd_19purple_dot.png differ diff --git a/data/themes/default/lcd_21pink.png b/data/themes/default/lcd_21pink.png index c2009eedac6..719730427bc 100644 Binary files a/data/themes/default/lcd_21pink.png and b/data/themes/default/lcd_21pink.png differ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 854d4a4c39e..e05d526533b 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -3,12 +3,13 @@ ********************/ /* most foreground text items */ -QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar { +QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar, QCheckBox { color: #d1d8e4; } QTreeView { outline: none; + alternate-background-color: #111314; } QTreeWidget::item { @@ -56,6 +57,7 @@ lmms--gui--AutomationEditor { qproperty-backgroundShade: rgba(255, 255, 255, 15); qproperty-nodeInValueColor: rgba(103, 73, 194, 150); qproperty-nodeOutValueColor: rgba(125, 40, 40, 150); + qproperty-nodeTangentLineColor: rgba(200, 200, 200, 255); qproperty-crossColor: rgba(215, 210, 254, 150); /* Grid colors */ qproperty-lineColor: #292929; @@ -64,6 +66,9 @@ lmms--gui--AutomationEditor { qproperty-graphColor: rgba(69,42,153,180); qproperty-scaleColor: #262b30; + qproperty-ghostNoteColor: rgba(248, 248, 255, 125); + qproperty-detuningNoteColor: rgba(248, 11, 11, 125); + qproperty-ghostSampleColor: rgba(125, 125, 125, 125); } /* text box */ @@ -173,6 +178,7 @@ lmms--gui--PianoRoll { qproperty-backgroundShade: rgba(255, 255, 255, 10); qproperty-noteModeColor: #0bd556; qproperty-noteColor: #0bd556; + qproperty-stepNoteColor: #9b1313; qproperty-noteTextColor: #ffffff; qproperty-noteOpacity: 165; qproperty-noteBorders: false; /* boolean property, set false to have borderless notes */ @@ -235,7 +241,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; - background: url(resources:cpuload_bg.png); + background: url("resources:cpuload_bg.png"); qproperty-stepSize: 1; } @@ -354,16 +360,16 @@ QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { margin-left: 3px; } -QScrollBar::left-arrow:horizontal { background-image: url(resources:sbarrow_left.png);} -QScrollBar::right-arrow:horizontal { background-image: url(resources:sbarrow_right.png);} -QScrollBar::up-arrow:vertical { background-image: url(resources:sbarrow_up.png);} -QScrollBar::down-arrow:vertical { background-image: url(resources:sbarrow_down.png);} -QScrollBar::left-arrow:horizontal:disabled { background-image: url(resources:sbarrow_left_d.png);} -QScrollBar::right-arrow:horizontal:disabled { background-image: url(resources:sbarrow_right_d.png);} -QScrollBar::up-arrow:vertical:disabled { background-image: url(resources:sbarrow_up_d.png);} -QScrollBar::down-arrow:vertical:disabled { background-image: url(resources:sbarrow_down_d.png);} -lmms--gui--EffectRackView QScrollBar::up-arrow:vertical:disabled { background-image: url(resources:sbarrow_up.png);} -lmms--gui--EffectRackView QScrollBar::down-arrow:vertical:disabled { background-image: url(resources:sbarrow_down.png);} +QScrollBar::left-arrow:horizontal { background-image: url("resources:sbarrow_left.png");} +QScrollBar::right-arrow:horizontal { background-image: url("resources:sbarrow_right.png");} +QScrollBar::up-arrow:vertical { background-image: url("resources:sbarrow_up.png");} +QScrollBar::down-arrow:vertical { background-image: url("resources:sbarrow_down.png");} +QScrollBar::left-arrow:horizontal:disabled { background-image: url("resources:sbarrow_left_d.png");} +QScrollBar::right-arrow:horizontal:disabled { background-image: url("resources:sbarrow_right_d.png");} +QScrollBar::up-arrow:vertical:disabled { background-image: url("resources:sbarrow_up_d.png");} +QScrollBar::down-arrow:vertical:disabled { background-image: url("resources:sbarrow_down_d.png");} +lmms--gui--EffectRackView QScrollBar::up-arrow:vertical:disabled { background-image: url("resources:sbarrow_up.png");} +lmms--gui--EffectRackView QScrollBar::down-arrow:vertical:disabled { background-image: url("resources:sbarrow_down.png");} /* background for song editor and pattern editor */ @@ -400,7 +406,7 @@ lmms--gui--TrackOperationsWidget > QPushButton { } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { - image: url(resources:trackop.png); + image: url("resources:trackop.png"); subcontrol-origin: padding; subcontrol-position: center; position: relative; @@ -409,7 +415,7 @@ lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:pressed, lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:checked { - image: url(resources:trackop.png); + image: url("resources:trackop.png"); position: relative; top: 2px; } @@ -432,7 +438,7 @@ lmms--gui--AutomatableSlider::groove:vertical { lmms--gui--AutomatableSlider::handle:vertical { background: none; - border-image: url(resources:main_slider.png); + border-image: url("resources:main_slider.png"); width: 26px; height: 10px; border-radius: 2px; @@ -451,7 +457,7 @@ lmms--gui--AutomatableSlider::groove:horizontal { lmms--gui--AutomatableSlider::handle:horizontal { background: none; - border-image: url(resources:horizontal_slider.png); + border-image: url("resources:horizontal_slider.png"); width: 10px; height: 26px; border-radius: 2px; @@ -464,6 +470,10 @@ lmms--gui--EffectSelectDialog QScrollArea { background: #262b30; } +lmms--gui--SetupDialog QScrollArea { + border: 0px; +} + /* the inner boxes in LADSPA effect windows */ lmms--gui--EffectControlDialog QGroupBox { @@ -537,7 +547,7 @@ QToolButton:checked { border-top: 1px solid #1b1f22; border-bottom: 1px solid #4a515e; background: qlineargradient(spread:reflect, x1:0, y1:0, x2:0, y2:1, stop:0 #1b1f22, stop:1 #13161a); - background-image: url(resources:shadow_p.png); + background-image: url("resources:shadow_p.png"); } /* buttons with combined menu */ @@ -586,7 +596,7 @@ lmms--gui--TrackLabelButton:pressed { lmms--gui--TrackLabelButton:checked { border: 1px solid #485059; background: #1C1F24; - background-image: url(resources:track_shadow_p.png); + background-image: url("resources:track_shadow_p.png"); border-radius: none; font-size: 11px; font-weight: normal; @@ -596,7 +606,7 @@ lmms--gui--TrackLabelButton:checked { lmms--gui--TrackLabelButton:checked:pressed { border: 1px solid #2f353b; background: #0e1012; - background-image: url(resources:track_shadow_p.png); + background-image: url("resources:track_shadow_p.png"); font-size: 11px; padding: 2px 1px; font-weight: solid; @@ -713,10 +723,6 @@ lmms--gui--TimeLineWidget { qproperty-barNumberColor: rgb( 192, 192, 192 ); } -QTreeView { - alternate-background-color: #111314; -} - lmms--gui--TrackContainerView QLabel { background: none; @@ -941,6 +947,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--SlicerTView lmms--gui--Knob { + color: rgb(162, 128, 226); + qproperty-outerColor: rgb( 162, 128, 226 ); + qproperty-innerRadius: 1; + qproperty-outerRadius: 11; + qproperty-lineWidth: 3; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; @@ -1017,6 +1031,11 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--BarModelEditor { + qproperty-backgroundBrush: rgba(28, 73, 51, 255); + qproperty-barBrush: rgba(17, 136, 71, 255); +} + /* palette information */ lmms--gui--LmmsPalette { diff --git a/include/AudioDeviceSetupWidget.h b/include/AudioDeviceSetupWidget.h index f56fa07a6ee..acc99602dba 100644 --- a/include/AudioDeviceSetupWidget.h +++ b/include/AudioDeviceSetupWidget.h @@ -2,6 +2,7 @@ * AudioDeviceSetupWidget.h - Base class for audio device setup widgets * * Copyright (c) 2004-2015 Tobias Doerffel + * Copyright (c) 2023- Michael Gregorius * * This file is part of LMMS - https://lmms.io * @@ -25,12 +26,12 @@ #ifndef LMMS_GUI_AUDIO_DEVICE_SETUP_WIDGET_H #define LMMS_GUI_AUDIO_DEVICE_SETUP_WIDGET_H -#include "TabWidget.h" +#include namespace lmms::gui { -class AudioDeviceSetupWidget : public TabWidget +class AudioDeviceSetupWidget : public QGroupBox { Q_OBJECT public: diff --git a/include/AudioEngine.h b/include/AudioEngine.h index d3d0d025ffc..67c2edd867a 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -25,14 +25,13 @@ #ifndef LMMS_AUDIO_ENGINE_H #define LMMS_AUDIO_ENGINE_H -#include - -#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) - #include +#ifdef __MINGW32__ +#include +#else +#include #endif #include -#include #include #include @@ -420,10 +419,6 @@ class LMMS_EXPORT AudioEngine : public QObject void clearInternal(); - //! Called by the audio thread to give control to other threads, - //! such that they can do changes in the model (like e.g. removing effects) - void runChangesInModel(); - bool m_renderOnly; std::vector m_audioPorts; @@ -453,8 +448,6 @@ class LMMS_EXPORT AudioEngine : public QObject struct qualitySettings m_qualitySettings; float m_masterGain; - bool m_isProcessing; - // audio device stuff void doSetAudioDevice( AudioDevice *_dev ); AudioDevice * m_audioDev; @@ -476,19 +469,7 @@ class LMMS_EXPORT AudioEngine : public QObject bool m_clearSignal; - bool m_changesSignal; - unsigned int m_changes; - QMutex m_changesMutex; -#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) - QRecursiveMutex m_doChangesMutex; -#else - QMutex m_doChangesMutex; -#endif - QMutex m_waitChangesMutex; - QWaitCondition m_changesAudioEngineCondition; - QWaitCondition m_changesRequestCondition; - - bool m_waitingForWrite; + std::mutex m_changeMutex; friend class Engine; friend class AudioEngineWorkerThread; diff --git a/include/AudioJack.h b/include/AudioJack.h index 164258e5fbd..6efb262ed40 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -57,42 +57,37 @@ class AudioJack : public QObject, public AudioDevice { Q_OBJECT public: - AudioJack( bool & _success_ful, AudioEngine* audioEngine ); + AudioJack(bool& successful, AudioEngine* audioEngine); ~AudioJack() override; // this is to allow the jack midi connection to use the same jack client connection // the jack callback is handled here, we call the midi client so that it can read // it's midi data during the callback - AudioJack * addMidiClient(MidiJack *midiClient); + AudioJack* addMidiClient(MidiJack* midiClient); void removeMidiClient() { m_midiClient = nullptr; } - jack_client_t * jackClient() {return m_client;}; + jack_client_t* jackClient() { return m_client; }; inline static QString name() { - return QT_TRANSLATE_NOOP( "AudioDeviceSetupWidget", - "JACK (JACK Audio Connection Kit)" ); + return QT_TRANSLATE_NOOP("AudioDeviceSetupWidget", "JACK (JACK Audio Connection Kit)"); } - -class setupWidget : public gui::AudioDeviceSetupWidget + class setupWidget : public gui::AudioDeviceSetupWidget { public: - setupWidget( QWidget * _parent ); + setupWidget(QWidget* parent); ~setupWidget() override; void saveSettings() override; private: - QLineEdit * m_clientName; - gui::LcdSpinBox * m_channels; - - } ; - + QLineEdit* m_clientName; + gui::LcdSpinBox* m_channels; + }; private slots: void restartAfterZombified(); - private: bool initJackClient(); @@ -100,45 +95,41 @@ private slots: void stopProcessing() override; void applyQualitySettings() override; - void registerPort( AudioPort * _port ) override; - void unregisterPort( AudioPort * _port ) override; - void renamePort( AudioPort * _port ) override; + void registerPort(AudioPort* port) override; + void unregisterPort(AudioPort* port) override; + void renamePort(AudioPort* port) override; - int processCallback( jack_nframes_t _nframes, void * _udata ); + int processCallback(jack_nframes_t nframes); - static int staticProcessCallback( jack_nframes_t _nframes, - void * _udata ); - static void shutdownCallback( void * _udata ); + static int staticProcessCallback(jack_nframes_t nframes, void* udata); + static void shutdownCallback(void* _udata); - - jack_client_t * m_client; + jack_client_t* m_client; bool m_active; std::atomic m_stopped; - std::atomic m_midiClient; - std::vector m_outputPorts; - jack_default_audio_sample_t * * m_tempOutBufs; - surroundSampleFrame * m_outBuf; + std::atomic m_midiClient; + std::vector m_outputPorts; + jack_default_audio_sample_t** m_tempOutBufs; + surroundSampleFrame* m_outBuf; f_cnt_t m_framesDoneInCurBuf; f_cnt_t m_framesToDoInCurBuf; - #ifdef AUDIO_PORT_SUPPORT struct StereoPort { - jack_port_t * ports[2]; - } ; + jack_port_t* ports[2]; + }; - using JackPortMap = QMap; + using JackPortMap = QMap; JackPortMap m_portMap; #endif signals: void zombified(); - -} ; +}; } // namespace lmms diff --git a/include/AutomationClip.h b/include/AutomationClip.h index ceb5611c958..0b49978c7f7 100644 --- a/include/AutomationClip.h +++ b/include/AutomationClip.h @@ -46,6 +46,7 @@ class TimePos; namespace gui { class AutomationClipView; +class AutomationEditor; } // namespace gui @@ -111,6 +112,13 @@ class LMMS_EXPORT AutomationClip : public Clip void resetNodes(const int tick0, const int tick1); + /** + * @brief Resets the tangents from the nodes between the given ticks + * @param Int first tick of the range + * @param Int second tick of the range + */ + void resetTangents(const int tick0, const int tick1); + void recordValue(TimePos time, float value); TimePos setDragValue( const TimePos & time, @@ -151,6 +159,17 @@ class LMMS_EXPORT AutomationClip : public Clip return m_timeMap.isEmpty() == false; } + static bool supportsTangentEditing(ProgressionType pType) + { + // Update function if we have new progression types that support tangent editing + return pType == ProgressionType::CubicHermite; + } + + inline bool canEditTangents() const + { + return supportsTangentEditing(m_progressionType); + } + float valueAt( const TimePos & _time ) const; float *valuesAfter( const TimePos & _time ) const; @@ -219,6 +238,9 @@ public slots: bool m_dragging; bool m_dragKeepOutValue; // Should we keep the current dragged node's outValue? float m_dragOutValue; // The outValue of the dragged node's + bool m_dragLockedTan; // If the dragged node has it's tangents locked + float m_dragInTan; // The dragged node's inTangent + float m_dragOutTan; // The dragged node's outTangent bool m_isRecording; float m_lastRecordedValue; @@ -230,6 +252,7 @@ public slots: friend class gui::AutomationClipView; friend class AutomationNode; + friend class gui::AutomationEditor; } ; @@ -261,6 +284,11 @@ inline float OUTTAN(AutomationClip::TimemapIterator it) return it->getOutTangent(); } +inline float LOCKEDTAN(AutomationClip::TimemapIterator it) +{ + return it->lockedTangents(); +} + inline int POS(AutomationClip::TimemapIterator it) { return it.key(); diff --git a/include/AutomationClipView.h b/include/AutomationClipView.h index a20e2ce2822..bdd2f056872 100644 --- a/include/AutomationClipView.h +++ b/include/AutomationClipView.h @@ -74,9 +74,6 @@ protected slots: QPixmap m_paintPixmap; QStaticText m_staticTextName; - - static QPixmap * s_clip_rec; - void scaleTimemapToFit( float oldMin, float oldMax ); } ; diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index ecefa8b26f1..1110e8e4c9e 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -26,16 +26,18 @@ #ifndef LMMS_GUI_AUTOMATION_EDITOR_H #define LMMS_GUI_AUTOMATION_EDITOR_H +#include #include #include +#include "AutomationClip.h" +#include "ComboBoxModel.h" #include "Editor.h" - -#include "lmms_basics.h" #include "JournallingObject.h" +#include "MidiClip.h" +#include "SampleClip.h" #include "TimePos.h" -#include "AutomationClip.h" -#include "ComboBoxModel.h" +#include "lmms_basics.h" class QPainter; class QPixmap; @@ -63,12 +65,18 @@ class AutomationEditor : public QWidget, public JournallingObject Q_PROPERTY(QColor lineColor MEMBER m_lineColor) Q_PROPERTY(QColor nodeInValueColor MEMBER m_nodeInValueColor) Q_PROPERTY(QColor nodeOutValueColor MEMBER m_nodeOutValueColor) + Q_PROPERTY(QColor nodeTangentLineColor MEMBER m_nodeTangentLineColor) Q_PROPERTY(QBrush scaleColor MEMBER m_scaleColor) Q_PROPERTY(QBrush graphColor MEMBER m_graphColor) Q_PROPERTY(QColor crossColor MEMBER m_crossColor) Q_PROPERTY(QColor backgroundShade MEMBER m_backgroundShade) + Q_PROPERTY(QColor ghostNoteColor MEMBER m_ghostNoteColor) + Q_PROPERTY(QColor detuningNoteColor MEMBER m_detuningNoteColor) + Q_PROPERTY(QColor ghostSampleColor MEMBER m_ghostSampleColor) public: void setCurrentClip(AutomationClip * new_clip); + void setGhostMidiClip(MidiClip* newMidiClip); + void setGhostSample(SampleClip* newSample); inline const AutomationClip * currentClip() const { @@ -91,7 +99,8 @@ class AutomationEditor : public QWidget, public JournallingObject { Draw, Erase, - DrawOutValues + DrawOutValues, + EditTangents }; public slots: @@ -118,6 +127,13 @@ public slots: inline void drawLevelTick(QPainter & p, int tick, float value); timeMap::iterator getNodeAt(int x, int y, bool outValue = false, int r = 5); + /** + * @brief Given a mouse X coordinate, returns a timeMap::iterator that points to + * the closest node. + * @param Int X coordinate + * @return timeMap::iterator with the closest node or timeMap.end() if there are no nodes. + */ + timeMap::iterator getClosestNode(int x); void drawLine( int x0, float y0, int x1, float y1 ); bool fineTuneValue(timeMap::iterator node, bool editingOutValue); @@ -133,6 +149,12 @@ protected slots: void setEditMode(int mode); void setProgressionType(AutomationClip::ProgressionType type); + /** + * @brief This method handles the AutomationEditorWindow event of changing + * progression types. After that, it calls updateEditTanButton so the edit + * tangents button is updated accordingly + * @param Int New progression type + */ void setProgressionType(int type); void setTension(); @@ -144,6 +166,13 @@ protected slots: /// Updates the clip's quantization using the current user selected value. void setQuantization(); + void resetGhostNotes() + { + m_ghostNotes = nullptr; + m_ghostSample = nullptr; + update(); + } + private: enum class Action @@ -153,7 +182,9 @@ protected slots: EraseValues, MoveOutValue, ResetOutValues, - DrawLine + DrawLine, + MoveTangent, + ResetTangents } ; // some constants... @@ -166,16 +197,23 @@ protected slots: static const int VALUES_WIDTH = 64; + static const int NOTE_HEIGHT = 10; // height of individual notes + static const int NOTE_MARGIN = 40; // total border margin for notes + static const int MIN_NOTE_RANGE = 20; // min number of keys for fixed size + static const int SAMPLE_MARGIN = 40; + static constexpr int MAX_SAMPLE_HEIGHT = 400; // constexpr for use in min + AutomationEditor(); AutomationEditor( const AutomationEditor & ); ~AutomationEditor() override; - static QPixmap * s_toolDraw; - static QPixmap * s_toolErase; - static QPixmap * s_toolDrawOut; - static QPixmap * s_toolMove; - static QPixmap * s_toolYFlip; - static QPixmap * s_toolXFlip; + QPixmap m_toolDraw = embed::getIconPixmap("edit_draw"); + QPixmap m_toolErase = embed::getIconPixmap("edit_erase"); + QPixmap m_toolDrawOut = embed::getIconPixmap("edit_draw_outvalue"); + QPixmap m_toolEditTangents = embed::getIconPixmap("edit_tangent"); + QPixmap m_toolMove = embed::getIconPixmap("edit_move"); + QPixmap m_toolYFlip = embed::getIconPixmap("flip_y"); + QPixmap m_toolXFlip = embed::getIconPixmap("flip_x"); ComboBoxModel m_zoomingXModel; ComboBoxModel m_zoomingYModel; @@ -193,6 +231,10 @@ protected slots: float m_bottomLevel; float m_topLevel; + MidiClip* m_ghostNotes = nullptr; + QPointer m_ghostSample = nullptr; // QPointer to set to nullptr on deletion + bool m_renderSample = false; + void centerTopBottomScroll(); void updateTopBottomLevels(); @@ -215,6 +257,11 @@ protected slots: // Time position (key) of automation node whose outValue is being dragged int m_draggedOutValueKey; + // The tick from the node whose tangent is being dragged + int m_draggedTangentTick; + // Whether the tangent being dragged is the InTangent or OutTangent + bool m_draggedOutTangent; + EditMode m_editMode; bool m_mouseDownLeft; @@ -225,6 +272,7 @@ protected slots: void drawCross(QPainter & p ); void drawAutomationPoint( QPainter & p, timeMap::iterator it ); + void drawAutomationTangents(QPainter& p, timeMap::iterator it); bool inPatternEditor(); QColor m_barLineColor; @@ -233,9 +281,13 @@ protected slots: QBrush m_graphColor; QColor m_nodeInValueColor; QColor m_nodeOutValueColor; + QColor m_nodeTangentLineColor; QBrush m_scaleColor; QColor m_crossColor; QColor m_backgroundShade; + QColor m_ghostNoteColor; + QColor m_detuningNoteColor; + QColor m_ghostSampleColor; friend class AutomationEditorWindow; @@ -259,6 +311,9 @@ class AutomationEditorWindow : public Editor ~AutomationEditorWindow() override = default; void setCurrentClip(AutomationClip* clip); + void setGhostMidiClip(MidiClip* clip) { m_editor->setGhostMidiClip(clip); }; + void setGhostSample(SampleClip* newSample) { m_editor->setGhostSample(newSample); }; + const AutomationClip* currentClip(); void dropEvent( QDropEvent * _de ) override; @@ -285,8 +340,21 @@ protected slots: private slots: void updateWindowTitle(); + void setProgressionType(int progType); + /** + * @brief The Edit Tangent edit mode should only be available for + * Cubic Hermite progressions, so this method is responsable for disabling it + * for other edit modes and reenabling it when it changes back to the Edit Tangent + * mode. + */ + void updateEditTanButton(); private: + QAction* m_drawAction; + QAction* m_eraseAction; + QAction* m_drawOutAction; + QAction* m_editTanAction; + QAction* m_discreteAction; QAction* m_linearAction; QAction* m_cubicHermiteAction; @@ -299,6 +367,8 @@ private slots: ComboBox * m_zoomingXComboBox; ComboBox * m_zoomingYComboBox; ComboBox * m_quantizeComboBox; + + QPushButton* m_resetGhostNotes; }; } // namespace gui diff --git a/include/AutomationNode.h b/include/AutomationNode.h index a922109e604..60154332f2c 100644 --- a/include/AutomationNode.h +++ b/include/AutomationNode.h @@ -125,6 +125,22 @@ class AutomationNode m_outTangent = tangent; } + /** + * @brief Checks if the tangents from the node are locked + */ + inline const bool lockedTangents() const + { + return m_lockedTangents; + } + + /** + * @brief Locks or Unlocks the tangents from this node + */ + inline void setLockedTangents(bool b) + { + m_lockedTangents = b; + } + /** * @brief Sets the clip this node belongs to * @param AutomationClip* clip that m_clip will be @@ -152,6 +168,11 @@ class AutomationNode // outValue are equal, inTangent and outTangent are equal too. float m_inTangent; float m_outTangent; + + // If the tangents were edited manually, this will be true. That way + // the tangents from this node will not be recalculated. It's set back + // to false if the tangents are reset. + bool m_lockedTangents; }; } // namespace lmms diff --git a/include/BarModelEditor.h b/include/BarModelEditor.h new file mode 100644 index 00000000000..79a320a7d05 --- /dev/null +++ b/include/BarModelEditor.h @@ -0,0 +1,76 @@ +/* + * BarModelEditor.h - edit model values using a bar display + * + * Copyright (c) 2023-now Michael Gregorius + * + * 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. + * + */ + +#pragma once + +#ifndef LMMS_GUI_BAR_MODEL_EDITOR_H +#define LMMS_GUI_BAR_MODEL_EDITOR_H + +#include "FloatModelEditorBase.h" + + +namespace lmms::gui +{ + +class LMMS_EXPORT BarModelEditor : public FloatModelEditorBase +{ + Q_OBJECT + +public: + Q_PROPERTY(QBrush backgroundBrush READ getBackgroundBrush WRITE setBackgroundBrush) + Q_PROPERTY(QBrush barBrush READ getBarBrush WRITE setBarBrush) + Q_PROPERTY(QColor textColor READ getTextColor WRITE setTextColor) + + BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + + // Define how the widget will behave in a layout + QSizePolicy sizePolicy() const; + + QSize minimumSizeHint() const override; + + QSize sizeHint() const override; + + QBrush const & getBackgroundBrush() const; + void setBackgroundBrush(QBrush const & backgroundBrush); + + QBrush const & getBarBrush() const; + void setBarBrush(QBrush const & barBrush); + + QColor const & getTextColor() const; + void setTextColor(QColor const & textColor); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QString const m_text; + + QBrush m_backgroundBrush; + QBrush m_barBrush; + QColor m_textColor; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_BAR_MODEL_EDITOR_H diff --git a/include/CPULoadWidget.h b/include/CPULoadWidget.h index dfa5bac73da..bed10b05eb7 100644 --- a/include/CPULoadWidget.h +++ b/include/CPULoadWidget.h @@ -68,7 +68,7 @@ protected slots: QTimer m_updateTimer; - int m_stepSize; + int m_stepSize = 1; } ; diff --git a/include/Clip.h b/include/Clip.h index 96394602fa0..0b540ccfbf7 100644 --- a/include/Clip.h +++ b/include/Clip.h @@ -25,6 +25,8 @@ #ifndef LMMS_CLIP_H #define LMMS_CLIP_H +#include + #include #include "AutomatableModel.h" @@ -109,24 +111,8 @@ class LMMS_EXPORT Clip : public Model, public JournallingObject return m_autoResize; } - QColor color() const - { - return m_color; - } - - void setColor( const QColor & c ) - { - m_color = c; - } - - bool hasColor(); - - void useCustomClipColor( bool b ); - - bool usesCustomClipColor() - { - return m_useCustomClipColor; - } + auto color() const -> const std::optional& { return m_color; } + void setColor(const std::optional& color); virtual void movePosition( const TimePos & pos ); virtual void changeLength( const TimePos & length ); @@ -177,8 +163,7 @@ public slots: bool m_selectViewOnCreate; - QColor m_color; - bool m_useCustomClipColor; + std::optional m_color; friend class ClipView; diff --git a/include/ClipView.h b/include/ClipView.h index 94225836725..14898db65b4 100644 --- a/include/ClipView.h +++ b/include/ClipView.h @@ -25,6 +25,7 @@ #ifndef LMMS_GUI_CLIP_VIEW_H #define LMMS_GUI_CLIP_VIEW_H +#include #include @@ -184,6 +185,7 @@ public slots: virtual void paintTextLabel(QString const & text, QPainter & painter); + auto hasCustomColor() const -> bool; protected slots: void updateLength(); @@ -241,7 +243,7 @@ protected slots: bool mouseMovedDistance( QMouseEvent * me, int distance ); TimePos draggedClipPos( QMouseEvent * me ); int knifeMarkerPos( QMouseEvent * me ); - void setColor(const QColor* color); + void setColor(const std::optional& color); //! Return true iff the clip could be split. Currently only implemented for samples virtual bool splitClip( const TimePos pos ){ return false; }; void updateCursor(QMouseEvent * me); diff --git a/include/Clipboard.h b/include/Clipboard.h index c6ae66ee873..cee40b33ae4 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -25,8 +25,10 @@ #ifndef LMMS_CLIPBOARD_H #define LMMS_CLIPBOARD_H -#include #include +#include + +#include "lmms_export.h" class QMimeData; @@ -44,7 +46,7 @@ namespace lmms::Clipboard bool hasFormat( MimeType mT ); // Helper methods for String data - void copyString( const QString & str, MimeType mT ); + void LMMS_EXPORT copyString(const QString& str, MimeType mT); QString getString( MimeType mT ); // Helper methods for String Pair data diff --git a/include/ComboBox.h b/include/ComboBox.h index 8153451e863..cc4ad68dd88 100644 --- a/include/ComboBox.h +++ b/include/ComboBox.h @@ -66,9 +66,9 @@ public slots: private: - static QPixmap* s_background; - static QPixmap* s_arrow; - static QPixmap* s_arrowSelected; + QPixmap m_background = embed::getIconPixmap("combobox_bg"); + QPixmap m_arrow = embed::getIconPixmap("combobox_arrow"); + QPixmap m_arrowSelected = embed::getIconPixmap("combobox_arrow_selected"); QMenu m_menu; diff --git a/include/DataFile.h b/include/DataFile.h index dc82315adb7..3f170622906 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -128,6 +128,8 @@ class LMMS_EXPORT DataFile : public QDomDocument void upgrade_bbTcoRename(); void upgrade_sampleAndHold(); void upgrade_midiCCIndexing(); + void upgrade_loopsRename(); + void upgrade_noteTypes(); // List of all upgrade methods static const std::vector UPGRADE_METHODS; diff --git a/include/Effect.h b/include/Effect.h index 1f566e0e923..f2fb6e80f49 100644 --- a/include/Effect.h +++ b/include/Effect.h @@ -157,6 +157,11 @@ class LMMS_EXPORT Effect : public Plugin { m_noRun = _state; } + + inline TempoSyncKnobModel* autoQuitModel() + { + return &m_autoQuitModel; + } EffectChain * effectChain() const { diff --git a/include/EnvelopeAndLfoView.h b/include/EnvelopeAndLfoView.h index b5c7a67d46c..d545aaa0687 100644 --- a/include/EnvelopeAndLfoView.h +++ b/include/EnvelopeAndLfoView.h @@ -29,6 +29,7 @@ #include #include "ModelView.h" +#include "embed.h" class QPaintEvent; class QPixmap; @@ -71,8 +72,8 @@ protected slots: private: - static QPixmap * s_envGraph; - static QPixmap * s_lfoGraph; + QPixmap m_envGraph = embed::getIconPixmap("envelope_graph"); + QPixmap m_lfoGraph = embed::getIconPixmap("lfo_graph"); EnvelopeAndLfoParameters * m_params; diff --git a/include/Fader.h b/include/Fader.h index b46bed11b47..c44d976a712 100644 --- a/include/Fader.h +++ b/include/Fader.h @@ -53,6 +53,7 @@ #include "AutomatableModelView.h" +#include "embed.h" namespace lmms::gui @@ -131,7 +132,7 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView float fRange = model()->maxValue() - model()->minValue(); float realVal = model()->value() - model()->minValue(); - return height() - ( ( height() - m_knob->height() ) * ( realVal / fRange ) ); + return height() - ((height() - m_knob.height()) * (realVal / fRange)); } void setPeak( float fPeak, float &targetPeak, float &persistentPeak, QElapsedTimer &lastPeakTimer ); @@ -151,13 +152,9 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView QElapsedTimer m_lastPeakTimer_L; QElapsedTimer m_lastPeakTimer_R; - static QPixmap * s_back; - static QPixmap * s_leds; - static QPixmap * s_knob; - - QPixmap * m_back; - QPixmap * m_leds; - QPixmap * m_knob; + QPixmap m_back = embed::getIconPixmap("fader_background"); + QPixmap m_leds = embed::getIconPixmap("fader_leds"); + QPixmap m_knob = embed::getIconPixmap("fader_knob"); bool m_levelsDisplayedInDBFS; diff --git a/include/FileBrowser.h b/include/FileBrowser.h index eafb827dac5..b0c8a519992 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -28,14 +28,18 @@ #include #include #include +#include "embed.h" + +#include "FileBrowserSearcher.h" +#include + #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include #endif #include - #include "SideBarWidget.h" - +#include "lmmsconfig.h" class QLineEdit; @@ -66,16 +70,31 @@ class FileBrowser : public SideBarWidget */ FileBrowser( const QString & directories, const QString & filter, const QString & title, const QPixmap & pm, - QWidget * parent, bool dirs_as_items = false, bool recurse = false, + QWidget * parent, bool dirs_as_items = false, const QString& userDir = "", const QString& factoryDir = ""); ~FileBrowser() override = default; + static QStringList directoryBlacklist() + { + static auto s_blacklist = QStringList{ +#ifdef LMMS_BUILD_LINUX + "/bin", "/boot", "/dev", "/etc", "/proc", "/run", "/sbin", + "/sys" +#endif +#ifdef LMMS_BUILD_WIN32 + "C:\\Windows" +#endif + }; + return s_blacklist; + } + static QDir::Filters dirFilters() { return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; } + static QDir::SortFlags sortFlags() { return QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase; } + private slots: void reloadTree(); - void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); - bool filterAndExpandItems(const QString & filter, QTreeWidgetItem * item = nullptr); + void expandItems(const QList& expandedDirs, QTreeWidgetItem* item = nullptr); void giveFocusToFilter(); private: @@ -86,15 +105,22 @@ private slots: void saveDirectoriesStates(); void restoreDirectoriesStates(); + void buildSearchTree(); + void onSearch(const QString& filter); + void toggleSearch(bool on); + FileBrowserTreeWidget * m_fileBrowserTreeWidget; + FileBrowserTreeWidget * m_searchTreeWidget; QLineEdit * m_filterEdit; + std::shared_ptr m_currentSearch; + QProgressBar* m_searchIndicator = nullptr; + QString m_directories; //!< Directories to search, split with '*' QString m_filter; //!< Filter as used in QDir::match() bool m_dirsAsItems; - bool m_recurse; void addContentCheckBox(); QCheckBox* m_showUserContent = nullptr; @@ -167,12 +193,10 @@ private slots: - class Directory : public QTreeWidgetItem { public: - Directory( const QString & filename, const QString & path, - const QString & filter ); + Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation = false); void update(); @@ -197,14 +221,12 @@ class Directory : public QTreeWidgetItem private: - void initPixmaps(); - bool addItems( const QString & path ); - static QPixmap * s_folderPixmap; - static QPixmap * s_folderOpenedPixmap; - static QPixmap * s_folderLockedPixmap; + QPixmap m_folderPixmap = embed::getIconPixmap("folder"); + QPixmap m_folderOpenedPixmap = embed::getIconPixmap("folder_opened"); + QPixmap m_folderLockedPixmap = embed::getIconPixmap("folder_locked"); //! Directories that lead here //! Initially, this is just set to the current path of a directory @@ -217,7 +239,7 @@ class Directory : public QTreeWidgetItem QString m_filter; int m_dirCount; - + bool m_disableEntryPopulation = false; } ; @@ -274,20 +296,13 @@ class FileItem : public QTreeWidgetItem QString extension(); static QString extension( const QString & file ); + static QString defaultFilters(); private: void initPixmaps(); void determineFileType(); - static QPixmap * s_projectFilePixmap; - static QPixmap * s_presetFilePixmap; - static QPixmap * s_sampleFilePixmap; - static QPixmap * s_soundfontFilePixmap; - static QPixmap * s_vstPluginFilePixmap; - static QPixmap * s_midiFilePixmap; - static QPixmap * s_unknownFilePixmap; - QString m_path; FileType m_type; FileHandling m_handling; diff --git a/include/FileBrowserSearcher.h b/include/FileBrowserSearcher.h new file mode 100644 index 00000000000..4f4d3ff1cba --- /dev/null +++ b/include/FileBrowserSearcher.h @@ -0,0 +1,148 @@ +/* + * FileBrowserSearcher.h - Batch processor for searching the filesystem + * + * Copyright (c) 2023 saker + * + * 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_FILE_BROWSER_SEARCHER_H +#define LMMS_FILE_BROWSER_SEARCHER_H + +#include +#include +#include +#include +#include + +#ifdef __MINGW32__ +#include +#include +#include +#else +#include +#include +#include +#endif + +namespace lmms::gui { + +//! An active object that handles searching for files that match a certain filter across the file system. +class FileBrowserSearcher +{ +public: + //! Number of milliseconds to wait for before a match should be processed by the user. + static constexpr int MillisecondsPerMatch = 1; + + //! The future object for FileBrowserSearcher. It is used to track the current state of search operations, as + // well as retrieve matches. + class SearchFuture + { + public: + //! Possible state values of the future object. + enum class State + { + Idle, + Running, + Cancelled, + Completed + }; + + //! Constructs a future object using the specified filter, paths, and valid file extensions in the Idle state. + SearchFuture(const QString& filter, const QStringList& paths, const QStringList& extensions) + : m_filter(filter) + , m_paths(paths) + , m_extensions(extensions) + { + } + + //! Retrieves a match from the match list. + auto match() -> QString + { + const auto lock = std::lock_guard{m_matchesMutex}; + return m_matches.empty() ? QString{} : m_matches.takeFirst(); + } + + //! Returns the current state of this future object. + auto state() -> State { return m_state; } + + //! Returns the filter used. + auto filter() -> const QString& { return m_filter; } + + //! Returns the paths to filter. + auto paths() -> const QStringList& { return m_paths; } + + //! Returns the valid file extensions. + auto extensions() -> const QStringList& { return m_extensions; } + + private: + //! Adds a match to the match list. + auto addMatch(const QString& match) -> void + { + const auto lock = std::lock_guard{m_matchesMutex}; + m_matches.append(match); + } + + QString m_filter; + QStringList m_paths; + QStringList m_extensions; + + QStringList m_matches; + std::mutex m_matchesMutex; + + std::atomic m_state = State::Idle; + + friend FileBrowserSearcher; + }; + + ~FileBrowserSearcher(); + + //! Enqueues a search to be ran by the worker thread. + //! Returns a future that the caller can use to track state and results of the operation. + auto search(const QString& filter, const QStringList& paths, const QStringList& extensions) + -> std::shared_ptr; + + //! Sends a signal to cancel a running search. + auto cancel() -> void { m_cancelRunningSearch = true; } + + //! Returns the global instance of the searcher object. + static auto instance() -> FileBrowserSearcher* + { + static auto s_instance = FileBrowserSearcher{}; + return &s_instance; + } + +private: + //! Event loop for the worker thread. + auto run() -> void; + + //! Using Depth-first search (DFS), filters the specified path and adds any matches to the future list. + auto process(SearchFuture* searchFuture, const QString& path) -> bool; + + std::queue> m_searchQueue; + std::atomic m_cancelRunningSearch = false; + + bool m_workerStopped = false; + std::mutex m_workerMutex; + std::condition_variable m_workerCond; + std::thread m_worker{[this] { run(); }}; +}; +} // namespace lmms::gui + +#endif // LMMS_FILE_BROWSER_SEARCHER_H diff --git a/include/Flags.h b/include/Flags.h index 76106dde660..62a5f8af8ba 100644 --- a/include/Flags.h +++ b/include/Flags.h @@ -48,8 +48,8 @@ class Flags m_value{value} {} - constexpr auto testAll(Flags flags) const -> bool { return *this & flags == flags; } - constexpr auto testAny(Flags flags) const -> bool { return *this & flags != Flags{}; } + constexpr auto testAll(Flags flags) const -> bool { return (*this & flags) == flags; } + constexpr auto testAny(Flags flags) const -> bool { return (*this & flags) != Flags{}; } constexpr auto testFlag(EnumType flag) const -> bool { return static_cast(*this & flag); } constexpr auto operator~() const -> Flags { return Flags{~m_value}; } diff --git a/include/FloatModelEditorBase.h b/include/FloatModelEditorBase.h new file mode 100644 index 00000000000..72f1450de5a --- /dev/null +++ b/include/FloatModelEditorBase.h @@ -0,0 +1,121 @@ +/* + * FloatModelEditorBase.h - Base editor for float models + * + * Copyright (c) 2004-2008 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * 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_GUI_FLOAT_MODEL_EDITOR_BASE_H +#define LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H + +#include +#include + +#include "AutomatableModelView.h" + + +namespace lmms::gui +{ + +class SimpleTextFloat; + +class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView +{ + Q_OBJECT + + mapPropertyFromModel(bool, isVolumeKnob, setVolumeKnob, m_volumeKnob); + mapPropertyFromModel(float, volumeRatio, setVolumeRatio, m_volumeRatio); + + void initUi(const QString & name); //!< to be called by ctors + +public: + enum class DirectionOfManipulation + { + Vertical, + Horizontal + }; + + FloatModelEditorBase(DirectionOfManipulation directionOfManipulation = DirectionOfManipulation::Vertical, QWidget * _parent = nullptr, const QString & _name = QString()); //!< default ctor + FloatModelEditorBase(const FloatModelEditorBase& other) = delete; + + // TODO: remove + inline void setHintText(const QString & txt_before, const QString & txt_after) + { + setDescription(txt_before); + setUnit(txt_after); + } + +signals: + void sliderPressed(); + void sliderReleased(); + void sliderMoved(float value); + + +protected: + void contextMenuEvent(QContextMenuEvent * me) override; + void dragEnterEvent(QDragEnterEvent * dee) override; + void dropEvent(QDropEvent * de) override; + void focusOutEvent(QFocusEvent * fe) override; + void mousePressEvent(QMouseEvent * me) override; + void mouseReleaseEvent(QMouseEvent * me) override; + void mouseMoveEvent(QMouseEvent * me) override; + void mouseDoubleClickEvent(QMouseEvent * me) override; + void paintEvent(QPaintEvent * me) override; + void wheelEvent(QWheelEvent * me) override; + + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual float getValue(const QPoint & p); + +private slots: + virtual void enterValue(); + void friendlyUpdate(); + void toggleScale(); + +private: + virtual QString displayValue() const; + + void doConnections() override; + + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); + void setPosition(const QPoint & p); + + inline float pageSize() const + { + return (model()->maxValue() - model()->minValue()) / 100.0f; + } + + static SimpleTextFloat * s_textFloat; + + BoolModel m_volumeKnob; + FloatModel m_volumeRatio; + + QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent + float m_leftOver; + bool m_buttonPressed; + + DirectionOfManipulation m_directionOfManipulation; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H diff --git a/include/Knob.h b/include/Knob.h index d5739bb1c3d..3c3339a6fe7 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -26,12 +26,9 @@ #define LMMS_GUI_KNOB_H #include -#include -#include -#include #include -#include "AutomatableModelView.h" +#include "FloatModelEditorBase.h" class QPixmap; @@ -50,7 +47,7 @@ enum class KnobType void convertPixmapToGrayScale(QPixmap &pixMap); -class LMMS_EXPORT Knob : public QWidget, public FloatModelView +class LMMS_EXPORT Knob : public FloatModelEditorBase { Q_OBJECT Q_ENUMS( KnobType ) @@ -72,9 +69,6 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView Q_PROPERTY(QColor arcActiveColor MEMBER m_arcActiveColor) Q_PROPERTY(QColor arcInactiveColor MEMBER m_arcInactiveColor) - mapPropertyFromModel(bool,isVolumeKnob,setVolumeKnob,m_volumeKnob); - mapPropertyFromModel(float,volumeRatio,setVolumeRatio,m_volumeRatio); - Q_PROPERTY(KnobType knobNum READ knobNum WRITE setknobNum) Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor) @@ -87,13 +81,6 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView Knob( QWidget * _parent = nullptr, const QString & _name = QString() ); //!< default ctor Knob( const Knob& other ) = delete; - // TODO: remove - inline void setHintText( const QString & _txt_before, - const QString & _txt_after ) - { - setDescription( _txt_before ); - setUnit( _txt_after ); - } void setLabel( const QString & txt ); void setHtmlLabel( const QString &htmltxt ); @@ -125,46 +112,16 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView void setTextColor( const QColor & c ); -signals: - void sliderPressed(); - void sliderReleased(); - void sliderMoved( float value ); - - protected: - void contextMenuEvent( QContextMenuEvent * _me ) override; - void dragEnterEvent( QDragEnterEvent * _dee ) override; - void dropEvent( QDropEvent * _de ) override; - void focusOutEvent( QFocusEvent * _fe ) override; - void mousePressEvent( QMouseEvent * _me ) override; - void mouseReleaseEvent( QMouseEvent * _me ) override; - void mouseMoveEvent( QMouseEvent * _me ) override; - void mouseDoubleClickEvent( QMouseEvent * _me ) override; void paintEvent( QPaintEvent * _me ) override; - 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: - virtual void enterValue(); - void friendlyUpdate(); - void toggleScale(); + void changeEvent(QEvent * ev) override; private: - virtual QString displayValue() const; - - void doConnections() override; - QLineF calculateLine( const QPointF & _mid, float _radius, float _innerRadius = 1) const; void drawKnob( QPainter * _p ); - void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); - void setPosition( const QPoint & _p ); bool updateAngle(); int angleFromValue( float value, float minValue, float maxValue, float totalAngle ) const @@ -172,25 +129,11 @@ private slots: return static_cast( ( value - 0.5 * ( minValue + maxValue ) ) / ( maxValue - minValue ) * m_totalAngle ) % 360; } - inline float pageSize() const - { - return ( model()->maxValue() - model()->minValue() ) / 100.0f; - } - - - static SimpleTextFloat * s_textFloat; - QString m_label; bool m_isHtmlLabel; QTextDocument* m_tdRenderer; std::unique_ptr m_knobPixmap; - BoolModel m_volumeKnob; - FloatModel m_volumeRatio; - - QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent - float m_leftOver; - bool m_buttonPressed; float m_totalAngle; int m_angle; @@ -211,9 +154,7 @@ private slots: QColor m_textColor; KnobType m_knobNum; - -} ; - +}; } // namespace lmms::gui diff --git a/include/LadspaControl.h b/include/LadspaControl.h index e4f0cd745ce..8af8f99231f 100644 --- a/include/LadspaControl.h +++ b/include/LadspaControl.h @@ -41,6 +41,7 @@ namespace gui { class LadspaControlView; +class LadspaMatrixControlDialog; } // namespace gui @@ -125,6 +126,7 @@ protected slots: friend class gui::LadspaControlView; + friend class gui::LadspaMatrixControlDialog; } ; diff --git a/include/LcdFloatSpinBox.h b/include/LcdFloatSpinBox.h index 74a870114a6..a87588b6230 100644 --- a/include/LcdFloatSpinBox.h +++ b/include/LcdFloatSpinBox.h @@ -49,6 +49,12 @@ class LMMS_EXPORT LcdFloatSpinBox : public QWidget, public FloatModelView } void setLabel(const QString &label) { m_label = label; } + + void setSeamless(bool left, bool right) + { + m_wholeDisplay.setSeamless(left, true); + m_fractionDisplay.setSeamless(true, right); + } public slots: virtual void update(); diff --git a/include/LcdWidget.h b/include/LcdWidget.h index f19c2c5838d..f900fea1500 100644 --- a/include/LcdWidget.h +++ b/include/LcdWidget.h @@ -47,10 +47,9 @@ class LMMS_EXPORT LcdWidget : public QWidget LcdWidget(int numDigits, const QString& style, QWidget* parent, const QString& name = QString(), bool leadingZero = false); - ~LcdWidget() override; - - void setValue( int value ); - void setLabel( const QString& label ); + void setValue(int value); + void setValue(float value); + void setLabel(const QString& label); void addTextForValue( int value, const QString& text ) { @@ -97,7 +96,7 @@ public slots: QString m_display; QString m_label; - QPixmap* m_lcdPixmap; + QPixmap m_lcdPixmap; QColor m_textColor; QColor m_textShadowColor; diff --git a/include/LedCheckBox.h b/include/LedCheckBox.h index e3629e143e7..4f23cd74b1b 100644 --- a/include/LedCheckBox.h +++ b/include/LedCheckBox.h @@ -47,13 +47,12 @@ class LMMS_EXPORT LedCheckBox : public AutomatableButton LedCheckBox( const QString & _txt, QWidget * _parent, const QString & _name = QString(), - LedColor _color = LedColor::Yellow ); + LedColor _color = LedColor::Yellow, + bool legacyMode = true); LedCheckBox( QWidget * _parent, const QString & _name = QString(), - LedColor _color = LedColor::Yellow ); - - ~LedCheckBox() override; - + LedColor _color = LedColor::Yellow, + bool legacyMode = true); inline const QString & text() { @@ -69,13 +68,18 @@ class LMMS_EXPORT LedCheckBox : public AutomatableButton private: - QPixmap * m_ledOnPixmap; - QPixmap * m_ledOffPixmap; + QPixmap m_ledOnPixmap; + QPixmap m_ledOffPixmap; QString m_text; + bool m_legacyMode; + void initUi( LedColor _color ); //!< to be called by ctors + void onTextUpdated(); //!< to be called when you updated @a m_text + void paintLegacy(QPaintEvent * p); + void paintNonLegacy(QPaintEvent * p); } ; diff --git a/include/Lv2Options.h b/include/Lv2Options.h index ca4fe2b7f48..603cdda43ac 100644 --- a/include/Lv2Options.h +++ b/include/Lv2Options.h @@ -84,6 +84,8 @@ class Lv2Options return m_options.data(); } + void clear(); + private: //! Initialize an option internally void initOption(LV2_URID key, diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 76fa5eec25b..1259aeedee4 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -66,6 +66,7 @@ namespace Lv2Ports //! For Mono effects, 1 Lv2ControlBase references 2 Lv2Proc. class Lv2Proc : public LinkedModelGroup { + friend class Lv2ProcSuspender; public: static Plugin::Type check(const LilvPlugin* plugin, std::vector &issues); @@ -175,7 +176,7 @@ class Lv2Proc : public LinkedModelGroup bool m_valid = true; const LilvPlugin* m_plugin; - LilvInstance* m_instance; + LilvInstance* m_instance = nullptr; Lv2Features m_features; // options diff --git a/include/Lv2UridMap.h b/include/Lv2UridMap.h index b8733023e5f..6c22aca3e40 100644 --- a/include/Lv2UridMap.h +++ b/include/Lv2UridMap.h @@ -55,8 +55,6 @@ class UridMap LV2_URID_Map m_mapFeature; LV2_URID_Unmap m_unmapFeature; - LV2_URID m_lastUrid = 0; - public: //! constructor; will set up the features UridMap(); diff --git a/include/Lv2ViewBase.h b/include/Lv2ViewBase.h index 3c8f1bc3faf..43086849cb6 100644 --- a/include/Lv2ViewBase.h +++ b/include/Lv2ViewBase.h @@ -37,7 +37,7 @@ class QPushButton; class QMdiSubWindow; - +class QLabel; namespace lmms { @@ -64,9 +64,25 @@ class Lv2ViewProc : public LinkedModelGroupView }; + + +class HelpWindowEventFilter : public QObject +{ + Q_OBJECT + class Lv2ViewBase* const m_viewBase; +protected: + bool eventFilter(QObject* obj, QEvent* event) override; +public: + HelpWindowEventFilter(class Lv2ViewBase* viewBase); +}; + + + + //! Base class for view for one Lv2 plugin class LMMS_EXPORT Lv2ViewBase : public LinkedModelGroupsView { + friend class HelpWindowEventFilter; protected: //! @param pluginWidget A child class which inherits QWidget Lv2ViewBase(class QWidget *pluginWidget, Lv2ControlBase *ctrlBase); @@ -79,6 +95,7 @@ class LMMS_EXPORT Lv2ViewBase : public LinkedModelGroupsView void toggleUI(); void toggleHelp(bool visible); + void closeHelpWindow(); // to be called by child virtuals //! Reconnect models if model changed @@ -94,12 +111,14 @@ class LMMS_EXPORT Lv2ViewBase : public LinkedModelGroupsView static AutoLilvNode uri(const char *uriStr); LinkedModelGroupView* getGroupView() override { return m_procView; } + void onHelpWindowClosed(); Lv2ViewProc* m_procView; //! Numbers of controls per row; must be multiple of 2 for mono effects const int m_colNum = 6; QMdiSubWindow* m_helpWindow = nullptr; + HelpWindowEventFilter m_helpWindowEventFilter; }; diff --git a/include/Lv2Worker.h b/include/Lv2Worker.h index 7931f8e7cde..90a3d9d4f8d 100644 --- a/include/Lv2Worker.h +++ b/include/Lv2Worker.h @@ -47,16 +47,17 @@ class Lv2Worker { public: // CTOR/DTOR/feature access - Lv2Worker(const LV2_Worker_Interface* iface, Semaphore* common_work_lock, bool threaded); + Lv2Worker(Semaphore* commonWorkLock, bool threaded); ~Lv2Worker(); - void setHandle(LV2_Handle handle) { m_handle = handle; } + void setHandle(LV2_Handle handle); + void setInterface(const LV2_Worker_Interface* newInterface); 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); } + if(m_interface->end_run) { m_interface->end_run(m_scheduleFeature.handle); } } // to be called only by static functions @@ -69,9 +70,9 @@ class Lv2Worker std::size_t bufferSize() const; //!< size of internal buffers // parameters - const LV2_Worker_Interface* m_iface; - bool m_threaded; - LV2_Handle m_handle; + const bool m_threaded; + const LV2_Worker_Interface* m_interface = nullptr; + LV2_Handle m_handle = nullptr; LV2_Worker_Schedule m_scheduleFeature; // threading/synchronization diff --git a/include/MidiClipView.h b/include/MidiClipView.h index 6558688b49b..4285bf9da0f 100644 --- a/include/MidiClipView.h +++ b/include/MidiClipView.h @@ -27,6 +27,7 @@ #include #include "ClipView.h" +#include "embed.h" namespace lmms { @@ -70,6 +71,7 @@ public slots: protected slots: void openInPianoRoll(); void setGhostInPianoRoll(); + void setGhostInAutomationEditor(); void resetName(); void changeName(); @@ -85,10 +87,10 @@ protected slots: private: - static QPixmap * s_stepBtnOn0; - static QPixmap * s_stepBtnOn200; - static QPixmap * s_stepBtnOff; - static QPixmap * s_stepBtnOffLight; + QPixmap m_stepBtnOn0 = embed::getIconPixmap("step_btn_on_0"); + QPixmap m_stepBtnOn200 = embed::getIconPixmap("step_btn_on_200"); + QPixmap m_stepBtnOff = embed::getIconPixmap("step_btn_off"); + QPixmap m_stepBtnOffLight = embed::getIconPixmap("step_btn_off_light"); MidiClip* m_clip; QPixmap m_paintPixmap; @@ -99,7 +101,7 @@ protected slots: QColor m_mutedNoteBorderColor; QStaticText m_staticTextName; - + bool m_legacySEPattern; } ; diff --git a/include/MidiEvent.h b/include/MidiEvent.h index 956c33fb389..9a14e427c44 100644 --- a/include/MidiEvent.h +++ b/include/MidiEvent.h @@ -212,7 +212,7 @@ class MidiEvent int32_t m_sysExDataLen; // len of m_sysExData } m_data; - const char* m_sysExData; + [[maybe_unused]] const char* m_sysExData; const void* m_sourcePort; // Stores the source of the MidiEvent: Internal or External (hardware controllers). diff --git a/include/MidiSetupWidget.h b/include/MidiSetupWidget.h index a61b606ac21..7b660601771 100644 --- a/include/MidiSetupWidget.h +++ b/include/MidiSetupWidget.h @@ -25,7 +25,7 @@ #ifndef LMMS_GUI_MIDI_SETUP_WIDGET_H #define LMMS_GUI_MIDI_SETUP_WIDGET_H -#include "TabWidget.h" +#include class QLineEdit; @@ -33,7 +33,7 @@ namespace lmms::gui { -class MidiSetupWidget : public TabWidget +class MidiSetupWidget : public QGroupBox { Q_OBJECT MidiSetupWidget( const QString & caption, const QString & configSection, diff --git a/include/Mixer.h b/include/Mixer.h index 35787a4144a..302492cab94 100644 --- a/include/Mixer.h +++ b/include/Mixer.h @@ -31,7 +31,7 @@ #include "ThreadableJob.h" #include - +#include #include namespace lmms @@ -76,26 +76,18 @@ class MixerChannel : public ThreadableJob bool requiresProcessing() const override { return true; } void unmuteForSolo(); + auto color() const -> const std::optional& { return m_color; } + void setColor(const std::optional& color) { m_color = color; } - void setColor (QColor newColor) - { - m_color = newColor; - m_hasColor = true; - } - - // TODO C++17 and above: use std::optional instead - QColor m_color; - bool m_hasColor; - - std::atomic_int m_dependenciesMet; void incrementDeps(); void processed(); private: void doProcessing() override; -}; + std::optional m_color; +}; class MixerRoute : public QObject { diff --git a/include/MixerLine.h b/include/MixerLine.h index 68a61728c46..655b30ec371 100644 --- a/include/MixerLine.h +++ b/include/MixerLine.h @@ -94,8 +94,6 @@ class MixerLine : public QWidget QColor m_strokeOuterInactive; QColor m_strokeInnerActive; QColor m_strokeInnerInactive; - static QPixmap * s_sendBgArrow; - static QPixmap * s_receiveBgArrow; bool m_inRename; QLineEdit * m_renameLineEdit; QGraphicsView * m_view; diff --git a/include/NoCopyNoMove.h b/include/NoCopyNoMove.h new file mode 100644 index 00000000000..d59ddee83c5 --- /dev/null +++ b/include/NoCopyNoMove.h @@ -0,0 +1,47 @@ +/* + * NoCopyNoMove.h - NoCopyNoMove class + * + * Copyright (c) 2023-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. + * + */ + +#ifndef LMMS_NOCOPYNOMOVE_H +#define LMMS_NOCOPYNOMOVE_H + +namespace lmms +{ + +/** + * Inherit this class to make your class non-copyable and non-movable + */ +class NoCopyNoMove +{ +protected: + NoCopyNoMove() = default; + NoCopyNoMove(const NoCopyNoMove& other) = delete; + NoCopyNoMove& operator=(const NoCopyNoMove& other) = delete; + NoCopyNoMove(NoCopyNoMove&& other) = delete; + NoCopyNoMove& operator=(NoCopyNoMove&& other) = delete; +}; + +} // namespace lmms + +#endif // LMMS_NOCOPYNOMOVE_H + diff --git a/include/Note.h b/include/Note.h index 2df196af20a..08cbce3dbeb 100644 --- a/include/Note.h +++ b/include/Note.h @@ -107,6 +107,16 @@ class LMMS_EXPORT Note : public SerializingObject Note( const Note & note ); ~Note() override; + // Note types + enum class Type + { + Regular = 0, + Step + }; + + Type type() const { return m_type; } + inline void setType(Type t) { m_type = t; } + // used by GUI inline void setSelected( const bool selected ) { m_selected = selected; } inline void setOldKey( const int oldKey ) { m_oldKey = oldKey; } @@ -253,6 +263,8 @@ class LMMS_EXPORT Note : public SerializingObject TimePos m_length; TimePos m_pos; DetuningHelper * m_detuning; + + Type m_type = Type::Regular; }; using NoteVector = std::vector; diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 38788180f8f..881732be1e3 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -73,6 +73,7 @@ class PianoRoll : public QWidget Q_PROPERTY(QColor lineColor MEMBER m_lineColor) Q_PROPERTY(QColor noteModeColor MEMBER m_noteModeColor) Q_PROPERTY(QColor noteColor MEMBER m_noteColor) + Q_PROPERTY(QColor stepNoteColor MEMBER m_stepNoteColor) Q_PROPERTY(QColor ghostNoteColor MEMBER m_ghostNoteColor) Q_PROPERTY(QColor noteTextColor MEMBER m_noteTextColor) Q_PROPERTY(QColor ghostNoteTextColor MEMBER m_ghostNoteTextColor) @@ -339,12 +340,12 @@ protected slots: static const int cm_scrollAmtHoriz = 10; static const int cm_scrollAmtVert = 1; - static QPixmap * s_toolDraw; - static QPixmap * s_toolErase; - static QPixmap * s_toolSelect; - static QPixmap * s_toolMove; - static QPixmap * s_toolOpen; - static QPixmap* s_toolKnife; + QPixmap m_toolDraw = embed::getIconPixmap("edit_draw"); + QPixmap m_toolErase = embed::getIconPixmap("edit_erase"); + QPixmap m_toolSelect = embed::getIconPixmap("edit_select"); + QPixmap m_toolMove = embed::getIconPixmap("edit_move"); + QPixmap m_toolOpen = embed::getIconPixmap("automation"); + QPixmap m_toolKnife = embed::getIconPixmap("edit_knife"); static std::array prKeyOrder; @@ -466,6 +467,7 @@ protected slots: QColor m_lineColor; QColor m_noteModeColor; QColor m_noteColor; + QColor m_stepNoteColor; QColor m_noteTextColor; QColor m_ghostNoteColor; QColor m_ghostNoteTextColor; diff --git a/include/PianoView.h b/include/PianoView.h index 6421ff4381c..3f8d8026f56 100644 --- a/include/PianoView.h +++ b/include/PianoView.h @@ -30,6 +30,7 @@ #include "AutomatableModel.h" #include "ModelView.h" +#include "embed.h" namespace lmms { @@ -73,12 +74,12 @@ class PianoView : public QWidget, public ModelView int getKeyHeight(int key_num) const; IntModel *getNearestMarker(int key, QString* title = nullptr); - static QPixmap * s_whiteKeyPm; - static QPixmap * s_blackKeyPm; - static QPixmap * s_whiteKeyPressedPm; - static QPixmap * s_blackKeyPressedPm; - static QPixmap * s_whiteKeyDisabledPm; - static QPixmap * s_blackKeyDisabledPm; + QPixmap m_whiteKeyPm = embed::getIconPixmap("white_key"); + QPixmap m_blackKeyPm = embed::getIconPixmap("black_key"); + QPixmap m_whiteKeyPressedPm = embed::getIconPixmap("white_key_pressed"); + QPixmap m_blackKeyPressedPm = embed::getIconPixmap("black_key_pressed"); + QPixmap m_whiteKeyDisabledPm = embed::getIconPixmap("white_key_disabled"); + QPixmap m_blackKeyDisabledPm = embed::getIconPixmap("black_key_disabled"); Piano * m_piano; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index b3f53d79092..4ff218fb0ef 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -47,6 +47,7 @@ class SampleClipView : public ClipView public slots: void updateSample(); void reverseSample(); + void setAutomationGhost(); diff --git a/include/SendButtonIndicator.h b/include/SendButtonIndicator.h index f1ee2dbca56..86f38318feb 100644 --- a/include/SendButtonIndicator.h +++ b/include/SendButtonIndicator.h @@ -26,6 +26,7 @@ #define LMMS_GUI_SEND_BUTTON_INDICATOR_H #include +#include "embed.h" namespace lmms @@ -53,8 +54,8 @@ class SendButtonIndicator : public QLabel MixerLine * m_parent; MixerView * m_mv; - static QPixmap * s_qpmOn; - static QPixmap * s_qpmOff; + QPixmap m_qpmOff = embed::getIconPixmap("mixer_send_off", 29, 20); + QPixmap m_qpmOn = embed::getIconPixmap("mixer_send_on", 29, 20); FloatModel * getSendModel(); }; diff --git a/include/SetupDialog.h b/include/SetupDialog.h index fa41325db79..882ca2bedce 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -30,12 +30,12 @@ #include "AudioDevice.h" #include "AudioDeviceSetupWidget.h" -#include "LedCheckBox.h" #include "lmmsconfig.h" #include "MidiClient.h" #include "MidiSetupWidget.h" +class QCheckBox; class QComboBox; class QLabel; class QLineEdit; @@ -156,14 +156,14 @@ private slots: bool m_enableRunningAutoSave; QSlider * m_saveIntervalSlider; QLabel * m_saveIntervalLbl; - LedCheckBox * m_autoSave; - LedCheckBox * m_runningAutoSave; + QCheckBox * m_autoSave; + QCheckBox * m_runningAutoSave; bool m_smoothScroll; bool m_animateAFP; QLabel * m_vstEmbedLbl; QComboBox* m_vstEmbedComboBox; QString m_vstEmbedMethod; - LedCheckBox * m_vstAlwaysOnTopCheckBox; + QCheckBox * m_vstAlwaysOnTopCheckBox; bool m_vstAlwaysOnTop; bool m_disableAutoQuit; diff --git a/include/SubWindow.h b/include/SubWindow.h index fdda6de4280..d1cc6a7af08 100644 --- a/include/SubWindow.h +++ b/include/SubWindow.h @@ -68,6 +68,8 @@ class LMMS_EXPORT SubWindow : public QMdiSubWindow void setActiveColor( const QBrush & b ); void setTextShadowColor( const QColor &c ); void setBorderColor( const QColor &c ); + int titleBarHeight() const; + int frameWidth() const; protected: // hook the QWidget move/resize events to update the tracked geometry diff --git a/include/TabBar.h b/include/TabBar.h index fa27032873c..29c100e0c7a 100644 --- a/include/TabBar.h +++ b/include/TabBar.h @@ -49,7 +49,12 @@ class LMMS_EXPORT TabBar : public QWidget TabButton * addTab( QWidget * _w, const QString & _text, int _id, bool _add_stretch = false, - bool _text_is_tooltip = false ); + bool _text_is_tooltip = false, + // TODO Remove fixWidgetToParentSize once it is used + // with false everywhere. + // At the time of writing it is only used in + // LadspaBrowser with default parameters. + bool fixWidgetToParentSize = true ); void removeTab( int _id ); inline void setExclusive( bool _on ) diff --git a/include/TempoSyncBarModelEditor.h b/include/TempoSyncBarModelEditor.h new file mode 100644 index 00000000000..c1b0bb26f7a --- /dev/null +++ b/include/TempoSyncBarModelEditor.h @@ -0,0 +1,90 @@ +/* + * TempoSyncBarModelEditor.h - adds bpm to ms conversion for the bar editor class + * + * Copyright (c) 2005-2008 Danny McRae + * Copyright (c) 2009-2014 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * 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_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H +#define LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H + +#include +#include + +#include "BarModelEditor.h" +#include "TempoSyncKnobModel.h" + +namespace lmms::gui +{ + +class MeterDialog; + +class LMMS_EXPORT TempoSyncBarModelEditor : public BarModelEditor +{ + Q_OBJECT +public: + TempoSyncBarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + ~TempoSyncBarModelEditor() override; + + const QString & syncDescription(); + void setSyncDescription(const QString & new_description); + + const QPixmap & syncIcon(); + void setSyncIcon(const QPixmap & new_pix); + + TempoSyncKnobModel * model() + { + return castModel(); + } + + void modelChanged() override; + + +signals: + void syncDescriptionChanged(const QString & new_description); + void syncIconChanged(); + + +protected: + void contextMenuEvent(QContextMenuEvent * me) override; + + +protected slots: + void updateDescAndIcon(); + void showCustom(); + + +private: + void updateTextDescription(); + void updateIcon(); + + +private: + QPixmap m_tempoSyncIcon; + QString m_tempoSyncDescription; + + QPointer m_custom; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H diff --git a/include/TempoSyncKnobModel.h b/include/TempoSyncKnobModel.h index 5cd2db067fa..af92a58aab0 100644 --- a/include/TempoSyncKnobModel.h +++ b/include/TempoSyncKnobModel.h @@ -82,6 +82,9 @@ class LMMS_EXPORT TempoSyncKnobModel : public FloatModel void setScale( float _new_scale ); + MeterModel & getCustomMeterModel() { return m_custom; } + MeterModel const & getCustomMeterModel() const { return m_custom; } + signals: void syncModeChanged( lmms::TempoSyncKnobModel::SyncMode _new_mode ); void scaleChanged( float _new_scale ); diff --git a/include/TimeLineWidget.h b/include/TimeLineWidget.h index 2e4ba6a97a4..2be73b77c49 100644 --- a/include/TimeLineWidget.h +++ b/include/TimeLineWidget.h @@ -28,6 +28,7 @@ #include #include "Song.h" +#include "embed.h" class QPixmap; @@ -205,7 +206,7 @@ public slots: private: - static QPixmap * s_posMarkerPixmap; + QPixmap m_posMarkerPixmap = embed::getIconPixmap("playpos_marker"); QColor m_inactiveLoopColor; QBrush m_inactiveLoopBrush; diff --git a/include/Track.h b/include/Track.h index 33d1ad23317..248d56b0471 100644 --- a/include/Track.h +++ b/include/Track.h @@ -32,6 +32,7 @@ #include "AutomatableModel.h" #include "JournallingObject.h" #include "lmms_basics.h" +#include namespace lmms @@ -188,15 +189,9 @@ class LMMS_EXPORT Track : public Model, public JournallingObject { return m_processingLock.tryLock(); } - - QColor color() - { - return m_color; - } - bool useColor() - { - return m_hasColor; - } + + auto color() const -> const std::optional& { return m_color; } + void setColor(const std::optional& color); bool isMutedBeforeSolo() const { @@ -219,9 +214,6 @@ public slots: void toggleSolo(); - void setColor(const QColor& c); - void resetColor(); - private: TrackContainer* m_trackContainer; Type m_type; @@ -241,8 +233,7 @@ public slots: QMutex m_processingLock; - QColor m_color; - bool m_hasColor; + std::optional m_color; friend class gui::TrackView; diff --git a/plugins/Amplifier/Amplifier.cpp b/plugins/Amplifier/Amplifier.cpp index 7de8fb18088..ac5fdf23b7d 100644 --- a/plugins/Amplifier/Amplifier.cpp +++ b/plugins/Amplifier/Amplifier.cpp @@ -36,9 +36,9 @@ extern "C" Plugin::Descriptor PLUGIN_EXPORT amplifier_plugin_descriptor = { - LMMS_STRINGIFY( PLUGIN_NAME ), + LMMS_STRINGIFY(PLUGIN_NAME), "Amplifier", - QT_TRANSLATE_NOOP( "PluginBrowser", "A native amplifier plugin" ), + QT_TRANSLATE_NOOP("PluginBrowser", "A native amplifier plugin"), "Vesa Kivimäki ", 0x0100, Plugin::Type::Effect, @@ -50,99 +50,61 @@ Plugin::Descriptor PLUGIN_EXPORT amplifier_plugin_descriptor = } - -AmplifierEffect::AmplifierEffect( Model* parent, const Descriptor::SubPluginFeatures::Key* key ) : - Effect( &lifier_plugin_descriptor, parent, key ), - m_ampControls( this ) +AmplifierEffect::AmplifierEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&lifier_plugin_descriptor, parent, key), + m_ampControls(this) { } - - - - - - -bool AmplifierEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) +bool AmplifierEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) { - if( !isEnabled() || !isRunning () ) - { - return( false ); - } + if (!isEnabled() || !isRunning()) { return false ; } double outSum = 0.0; const float d = dryLevel(); const float w = wetLevel(); - const ValueBuffer * volBuf = m_ampControls.m_volumeModel.valueBuffer(); - const ValueBuffer * panBuf = m_ampControls.m_panModel.valueBuffer(); - const ValueBuffer * leftBuf = m_ampControls.m_leftModel.valueBuffer(); - const ValueBuffer * rightBuf = m_ampControls.m_rightModel.valueBuffer(); + const ValueBuffer* volumeBuf = m_ampControls.m_volumeModel.valueBuffer(); + const ValueBuffer* panBuf = m_ampControls.m_panModel.valueBuffer(); + const ValueBuffer* leftBuf = m_ampControls.m_leftModel.valueBuffer(); + const ValueBuffer* rightBuf = m_ampControls.m_rightModel.valueBuffer(); - for( fpp_t f = 0; f < frames; ++f ) + for (fpp_t f = 0; f < frames; ++f) { -// qDebug( "offset %d, value %f", f, m_ampControls.m_volumeModel.value( f ) ); + const float volume = (volumeBuf ? volumeBuf->value(f) : m_ampControls.m_volumeModel.value()) * 0.01f; + const float pan = (panBuf ? panBuf->value(f) : m_ampControls.m_panModel.value()) * 0.01f; + const float left = (leftBuf ? leftBuf->value(f) : m_ampControls.m_leftModel.value()) * 0.01f; + const float right = (rightBuf ? rightBuf->value(f) : m_ampControls.m_rightModel.value()) * 0.01f; + + const float panLeft = std::min(1.0f, 1.0f - pan); + const float panRight = std::min(1.0f, 1.0f + pan); auto s = std::array{buf[f][0], buf[f][1]}; - // vol knob - if( volBuf ) - { - s[0] *= volBuf->value( f ) * 0.01f; - s[1] *= volBuf->value( f ) * 0.01f; - } - else - { - s[0] *= m_ampControls.m_volumeModel.value() * 0.01f; - s[1] *= m_ampControls.m_volumeModel.value() * 0.01f; - } - - // convert pan values to left/right values - const float pan = panBuf - ? panBuf->value( f ) - : m_ampControls.m_panModel.value(); - const float left1 = pan <= 0 - ? 1.0 - : 1.0 - pan * 0.01f; - const float right1 = pan >= 0 - ? 1.0 - : 1.0 + pan * 0.01f; - - // second stage amplification - const float left2 = leftBuf - ? leftBuf->value( f ) - : m_ampControls.m_leftModel.value(); - const float right2 = rightBuf - ? rightBuf->value( f ) - : m_ampControls.m_rightModel.value(); - - s[0] *= left1 * left2 * 0.01; - s[1] *= right1 * right2 * 0.01; + s[0] *= volume * left * panLeft; + s[1] *= volume * right * panRight; buf[f][0] = d * buf[f][0] + w * s[0]; buf[f][1] = d * buf[f][1] + w * s[1]; outSum += buf[f][0] * buf[f][0] + buf[f][1] * buf[f][1]; } - checkGate( outSum / frames ); + checkGate(outSum / frames); return isRunning(); } - - - extern "C" { // necessary for getting instance out of shared lib -PLUGIN_EXPORT Plugin * lmms_plugin_main( Model* parent, void* data ) +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data) { - return new AmplifierEffect( parent, static_cast( data ) ); + return new AmplifierEffect(parent, static_cast(data)); } } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/Amplifier/Amplifier.h b/plugins/Amplifier/Amplifier.h index 38fd07c6fb4..8a39ffeb616 100644 --- a/plugins/Amplifier/Amplifier.h +++ b/plugins/Amplifier/Amplifier.h @@ -23,9 +23,8 @@ * */ - -#ifndef AMPLIFIER_H -#define AMPLIFIER_H +#ifndef LMMS_AMPLIFIER_H +#define LMMS_AMPLIFIER_H #include "Effect.h" #include "AmplifierControls.h" @@ -36,24 +35,21 @@ namespace lmms class AmplifierEffect : public Effect { public: - AmplifierEffect( Model* parent, const Descriptor::SubPluginFeatures::Key* key ); + AmplifierEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); ~AmplifierEffect() override = default; - bool processAudioBuffer( sampleFrame* buf, const fpp_t frames ) override; + bool processAudioBuffer(sampleFrame* buf, const fpp_t frames) override; EffectControls* controls() override { return &m_ampControls; } - private: AmplifierControls m_ampControls; friend class AmplifierControls; - -} ; - +}; } // namespace lmms -#endif +#endif // LMMS_AMPLIFIER_H diff --git a/plugins/Amplifier/AmplifierControlDialog.cpp b/plugins/Amplifier/AmplifierControlDialog.cpp index ed9e98f29b4..1fbc3729a08 100644 --- a/plugins/Amplifier/AmplifierControlDialog.cpp +++ b/plugins/Amplifier/AmplifierControlDialog.cpp @@ -23,53 +23,38 @@ * */ - #include "AmplifierControlDialog.h" #include "AmplifierControls.h" #include "embed.h" #include "Knob.h" - namespace lmms::gui { - -AmplifierControlDialog::AmplifierControlDialog( AmplifierControls* controls ) : - EffectControlDialog( controls ) +AmplifierControlDialog::AmplifierControlDialog(AmplifierControls* controls) : + EffectControlDialog(controls) { - setAutoFillBackground( true ); + setAutoFillBackground(true); QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) ); - setPalette( pal ); - setFixedSize( 100, 110 ); - - auto volumeKnob = new Knob(KnobType::Bright26, this); - volumeKnob -> move( 16, 10 ); - volumeKnob -> setVolumeKnob( true ); - volumeKnob->setModel( &controls->m_volumeModel ); - volumeKnob->setLabel( tr( "VOL" ) ); - volumeKnob->setHintText( tr( "Volume:" ) , "%" ); - - auto panKnob = new Knob(KnobType::Bright26, this); - panKnob -> move( 57, 10 ); - panKnob->setModel( &controls->m_panModel ); - panKnob->setLabel( tr( "PAN" ) ); - panKnob->setHintText( tr( "Panning:" ) , "" ); - - auto leftKnob = new Knob(KnobType::Bright26, this); - leftKnob -> move( 16, 65 ); - leftKnob -> setVolumeKnob( true ); - leftKnob->setModel( &controls->m_leftModel ); - leftKnob->setLabel( tr( "LEFT" ) ); - leftKnob->setHintText( tr( "Left gain:" ) , "%" ); - - auto rightKnob = new Knob(KnobType::Bright26, this); - rightKnob -> move( 57, 65 ); - rightKnob -> setVolumeKnob( true ); - rightKnob->setModel( &controls->m_rightModel ); - rightKnob->setLabel( tr( "RIGHT" ) ); - rightKnob->setHintText( tr( "Right gain:" ) , "%" ); + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(100, 110); + + auto makeKnob = [this](int x, int y, const QString& label, const QString& hintText, const QString& unit, FloatModel* model, bool isVolume) + { + Knob* newKnob = new Knob(KnobType::Bright26, this); + newKnob->move(x, y); + newKnob->setModel(model); + newKnob->setLabel(label); + newKnob->setHintText(hintText, unit); + newKnob->setVolumeKnob(isVolume); + return newKnob; + }; + + makeKnob(16, 10, tr("VOL"), tr("Volume:"), "%", &controls->m_volumeModel, true); + makeKnob(57, 10, tr("PAN"), tr("Panning:"), "%", &controls->m_panModel, false); + makeKnob(16, 65, tr("LEFT"), tr("Left gain:"), "%", &controls->m_leftModel, true); + makeKnob(57, 65, tr("RIGHT"), tr("Right gain:"), "%", &controls->m_rightModel, true); } - -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/plugins/Amplifier/AmplifierControlDialog.h b/plugins/Amplifier/AmplifierControlDialog.h index ad0ed50ca2c..672830117ee 100644 --- a/plugins/Amplifier/AmplifierControlDialog.h +++ b/plugins/Amplifier/AmplifierControlDialog.h @@ -23,8 +23,8 @@ * */ -#ifndef AMPLIFIER_CONTROL_DIALOG_H -#define AMPLIFIER_CONTROL_DIALOG_H +#ifndef LMMS_GUI_AMPLIFIER_CONTROL_DIALOG_H +#define LMMS_GUI_AMPLIFIER_CONTROL_DIALOG_H #include "EffectControlDialog.h" @@ -32,23 +32,23 @@ namespace lmms { class AmplifierControls; - +class FloatModel; namespace gui { +class Knob; + class AmplifierControlDialog : public EffectControlDialog { Q_OBJECT public: - AmplifierControlDialog( AmplifierControls* controls ); + AmplifierControlDialog(AmplifierControls* controls); ~AmplifierControlDialog() override = default; - -} ; - +}; } // namespace gui } // namespace lmms -#endif +#endif // LMMS_GUI_AMPLIFIER_CONTROL_DIALOG_H diff --git a/plugins/Amplifier/AmplifierControls.cpp b/plugins/Amplifier/AmplifierControls.cpp index 30773046044..72960dd3b81 100644 --- a/plugins/Amplifier/AmplifierControls.cpp +++ b/plugins/Amplifier/AmplifierControls.cpp @@ -23,7 +23,6 @@ * */ - #include #include "AmplifierControls.h" @@ -32,51 +31,33 @@ namespace lmms { -AmplifierControls::AmplifierControls( AmplifierEffect* effect ) : - EffectControls( effect ), - m_effect( effect ), - m_volumeModel( 100.0f, 0.0f, 200.0f, 0.1f, this, tr( "Volume" ) ), - m_panModel( 0.0f, -100.0f, 100.0f, 0.1f, this, tr( "Panning" ) ), - m_leftModel( 100.0f, 0.0f, 200.0f, 0.1f, this, tr( "Left gain" ) ), - m_rightModel( 100.0f, 0.0f, 200.0f, 0.1f, this, tr( "Right gain" ) ) +AmplifierControls::AmplifierControls(AmplifierEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_volumeModel(100.0f, 0.0f, 200.0f, 0.1f, this, tr("Volume")), + m_panModel(0.0f, -100.0f, 100.0f, 0.1f, this, tr("Panning")), + m_leftModel(100.0f, 0.0f, 200.0f, 0.1f, this, tr("Left gain")), + m_rightModel(100.0f, 0.0f, 200.0f, 0.1f, this, tr("Right gain")) { -/* connect( &m_volumeModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) ); - connect( &m_panModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) ); - connect( &m_leftModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) ); - connect( &m_rightModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) );*/ } - - -void AmplifierControls::changeControl() +void AmplifierControls::loadSettings(const QDomElement& parent) { -// engine::getSong()->setModified(); + m_volumeModel.loadSettings(parent, "volume"); + m_panModel.loadSettings(parent, "pan"); + m_leftModel.loadSettings(parent, "left"); + m_rightModel.loadSettings(parent, "right"); } - - -void AmplifierControls::loadSettings( const QDomElement& _this ) +void AmplifierControls::saveSettings(QDomDocument& doc, QDomElement& parent) { - m_volumeModel.loadSettings( _this, "volume" ); - m_panModel.loadSettings( _this, "pan" ); - m_leftModel.loadSettings( _this, "left" ); - m_rightModel.loadSettings( _this, "right" ); -} - - - - -void AmplifierControls::saveSettings( QDomDocument& doc, QDomElement& _this ) -{ - m_volumeModel.saveSettings( doc, _this, "volume" ); - m_panModel.saveSettings( doc, _this, "pan" ); - m_leftModel.saveSettings( doc, _this, "left" ); - m_rightModel.saveSettings( doc, _this, "right" ); + m_volumeModel.saveSettings(doc, parent, "volume"); + m_panModel.saveSettings(doc, parent, "pan"); + m_leftModel.saveSettings(doc, parent, "left"); + m_rightModel.saveSettings(doc, parent, "right"); } } // namespace lmms - - diff --git a/plugins/Amplifier/AmplifierControls.h b/plugins/Amplifier/AmplifierControls.h index 573f6f8964c..6b5063dddfe 100644 --- a/plugins/Amplifier/AmplifierControls.h +++ b/plugins/Amplifier/AmplifierControls.h @@ -23,8 +23,8 @@ * */ -#ifndef AMPLIFIER_CONTROLS_H -#define AMPLIFIER_CONTROLS_H +#ifndef LMMS_AMPLIFIER_CONTROLS_H +#define LMMS_AMPLIFIER_CONTROLS_H #include "EffectControls.h" #include "AmplifierControlDialog.h" @@ -39,34 +39,24 @@ namespace gui class AmplifierControlDialog; } - class AmplifierControls : public EffectControls { Q_OBJECT public: - AmplifierControls( AmplifierEffect* effect ); + AmplifierControls(AmplifierEffect* effect); ~AmplifierControls() override = default; - void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; - void loadSettings( const QDomElement & _this ) override; + void saveSettings(QDomDocument& doc, QDomElement& parent) override; + void loadSettings(const QDomElement& parent) override; inline QString nodeName() const override { return "AmplifierControls"; } - - int controlCount() override - { - return 4; - } - gui::EffectControlDialog* createView() override { - return new gui::AmplifierControlDialog( this ); + return new gui::AmplifierControlDialog(this); } - - -private slots: - void changeControl(); + int controlCount() override { return 4; } private: AmplifierEffect* m_effect; @@ -77,10 +67,8 @@ private slots: friend class gui::AmplifierControlDialog; friend class AmplifierEffect; - -} ; - +}; } // namespace lmms -#endif +#endif // LMMS_AMPLIFIER_CONTROLS_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 6671022070c..459ff566c4f 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -24,6 +24,8 @@ #include "AudioFileProcessor.h" +#include + #include #include #include @@ -66,7 +68,11 @@ Plugin::Descriptor PLUGIN_EXPORT audiofileprocessor_plugin_descriptor = 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader( "logo" ), - "wav,ogg,ds,spx,au,voc,aif,aiff,flac,raw", + "wav,ogg,ds,spx,au,voc,aif,aiff,flac,raw" +#ifdef LMMS_HAVE_SNDFILE_MP3 + ",mp3" +#endif + , nullptr, } ; @@ -289,15 +295,24 @@ QString AudioFileProcessor::nodeName() const -int AudioFileProcessor::getBeatLen( NotePlayHandle * _n ) const +auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> int { + // If we can play indefinitely, use the default beat note duration + if (static_cast(m_loopModel.value()) != SampleBuffer::LoopMode::Off) { return 0; } + + // Otherwise, use the remaining sample duration const auto baseFreq = instrumentTrack()->baseFreq(); - const float freq_factor = baseFreq / _n->frequency() * - Engine::audioEngine()->processingSampleRate() / Engine::audioEngine()->baseSampleRate(); + const auto freqFactor = baseFreq / note->frequency() + * Engine::audioEngine()->processingSampleRate() + / Engine::audioEngine()->baseSampleRate(); - return static_cast( floorf( ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * freq_factor ) ); -} + const auto startFrame = m_nextPlayStartPoint >= m_sampleBuffer.endFrame() + ? m_sampleBuffer.startFrame() + : m_nextPlayStartPoint; + const auto duration = m_sampleBuffer.endFrame() - startFrame; + return static_cast(std::floor(duration * freqFactor)); +} @@ -436,19 +451,12 @@ namespace gui { -QPixmap * AudioFileProcessorView::s_artwork = nullptr; AudioFileProcessorView::AudioFileProcessorView( Instrument * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ) { - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( - "artwork" ) ); - } - m_openAudioFileButton = new PixmapButton( this ); m_openAudioFileButton->setCursor( QCursor( Qt::PointingHandCursor ) ); m_openAudioFileButton->move( 227, 72 ); @@ -634,7 +642,8 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) { QPainter p( this ); - p.drawPixmap( 0, 0, *s_artwork ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + p.drawPixmap(0, 0, s_artwork); auto a = castModel(); @@ -1160,14 +1169,19 @@ void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) return; } const double v = static_cast( _frames ) / m_sampleBuffer.frames(); - if( m_startKnob ) { - m_startKnob->slideBy( v, false ); - } - if( m_endKnob ) { - m_endKnob->slideBy( v, false ); + // update knobs in the right order + // to avoid them clamping each other + if (v < 0) + { + m_startKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_endKnob->slideBy(v, false); } - if( m_loopKnob ) { - m_loopKnob->slideBy( v, false ); + else + { + m_endKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_startKnob->slideBy(v, false); } } diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 5fed10862ff..4a5f21cc09a 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -67,7 +67,7 @@ class AudioFileProcessor : public Instrument QString nodeName() const override; - virtual int getBeatLen( NotePlayHandle * _n ) const; + auto beatLen(NotePlayHandle* note) const -> int override; f_cnt_t desiredReleaseFrames() const override { @@ -145,7 +145,6 @@ protected slots: private: virtual void modelChanged(); - static QPixmap * s_artwork; AudioFileProcessorWaveView * m_waveView; Knob * m_ampKnob; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9a71be4b823..04862cac1ba 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -9,10 +9,7 @@ IF(LMMS_BUILD_APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") ENDIF() -INCLUDE_DIRECTORIES( - ${SAMPLERATE_INCLUDE_DIRS} - "${CMAKE_BINARY_DIR}/src" -) +include_directories("${CMAKE_BINARY_DIR}/src") # See cmake/modules/PluginList.cmake FOREACH(PLUGIN ${PLUGIN_LIST}) diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp index 0fe13942083..e59562b0279 100755 --- a/plugins/Compressor/Compressor.cpp +++ b/plugins/Compressor/Compressor.cpp @@ -60,9 +60,6 @@ CompressorEffect::CompressorEffect(Model* parent, const Descriptor::SubPluginFea m_yL[0] = m_yL[1] = COMP_NOISE_FLOOR; - m_maxLookaheadVal[0] = 0; - m_maxLookaheadVal[1] = 0; - // 200 ms m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); @@ -183,9 +180,7 @@ void CompressorEffect::resizeRMS() void CompressorEffect::calcLookaheadLength() { - m_lookaheadLength = qMax(m_compressorControls.m_lookaheadLengthModel.value() * 0.001f * m_sampleRate, 1.f); - - m_preLookaheadLength = ceil(m_lookaheadDelayLength - m_lookaheadLength); + m_lookaheadLength = std::ceil((m_compressorControls.m_lookaheadLengthModel.value() / 1000.f) * m_sampleRate); } void CompressorEffect::calcThreshold() @@ -249,17 +244,10 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) m_gainResult[0] = m_gainResult[1] = 1; m_displayPeak[0] = m_displayPeak[1] = COMP_NOISE_FLOOR; m_displayGain[0] = m_displayGain[1] = COMP_NOISE_FLOOR; - std::fill(std::begin(m_lookaheadBuf[0]), std::end(m_lookaheadBuf[0]), 0); - std::fill(std::begin(m_lookaheadBuf[1]), std::end(m_lookaheadBuf[1]), 0); - m_lookaheadBufLoc[0] = 0; - m_lookaheadBufLoc[1] = 0; - std::fill(std::begin(m_preLookaheadBuf[0]), std::end(m_preLookaheadBuf[0]), 0); - std::fill(std::begin(m_preLookaheadBuf[1]), std::end(m_preLookaheadBuf[1]), 0); - m_preLookaheadBufLoc[0] = 0; - m_preLookaheadBufLoc[1] = 0; - std::fill(std::begin(m_inputBuf[0]), std::end(m_inputBuf[0]), 0); - std::fill(std::begin(m_inputBuf[1]), std::end(m_inputBuf[1]), 0); - m_inputBufLoc = 0; + std::fill(std::begin(m_scLookBuf[0]), std::end(m_scLookBuf[0]), COMP_NOISE_FLOOR); + std::fill(std::begin(m_scLookBuf[1]), std::end(m_scLookBuf[1]), COMP_NOISE_FLOOR); + std::fill(std::begin(m_inLookBuf[0]), std::end(m_inLookBuf[0]), 0); + std::fill(std::begin(m_inLookBuf[1]), std::end(m_inLookBuf[1]), 0); m_cleanedBuffers = true; } return false; @@ -318,7 +306,7 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) for (int i = 0; i < 2; i++) { - float inputValue = feedback ? m_prevOut[i] : s[i]; + float inputValue = (feedback && !lookahead) ? m_prevOut[i] : s[i]; // Calculate the crest factor of the audio by diving the peak by the RMS m_crestPeakVal[i] = qMax(qMax(COMP_NOISE_FLOOR, inputValue * inputValue), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); @@ -330,53 +318,6 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) // Grab the peak or RMS value inputValue = qMax(COMP_NOISE_FLOOR, peakmode ? std::abs(inputValue) : std::sqrt(m_rmsVal[i])); - // The following code uses math magic to semi-efficiently - // find the largest value in the lookahead buffer. - // This can probably be improved. - if (lookahead) - { - // Pre-lookahead delay, so the total delay always matches 20 ms - ++m_preLookaheadBufLoc[i]; - if (m_preLookaheadBufLoc[i] >= m_preLookaheadLength) - { - m_preLookaheadBufLoc[i] = 0; - } - const float tempInputValue = inputValue; - inputValue = m_preLookaheadBuf[i][m_preLookaheadBufLoc[i]]; - m_preLookaheadBuf[i][m_preLookaheadBufLoc[i]] = tempInputValue; - - - // Increment ring buffer location - ++m_lookaheadBufLoc[i]; - if (m_lookaheadBufLoc[i] >= m_lookaheadLength) - { - m_lookaheadBufLoc[i] = 0; - } - - m_lookaheadBuf[i][m_lookaheadBufLoc[i]] = inputValue; - - // If the new input value is larger than the stored maximum, - // store that as the maximum - if (inputValue >= m_maxLookaheadVal[i]) - { - m_maxLookaheadVal[i] = inputValue; - m_maxLookaheadTimer[i] = m_lookaheadLength; - } - - // Decrement timer. When the timer reaches 0, that means the - // stored maximum value has left the buffer and a new - // maximum value must be found. - if (--m_maxLookaheadTimer[i] <= 0) - { - m_maxLookaheadTimer[i] = std::distance(std::begin(m_lookaheadBuf[i]), - std::max_element(std::begin(m_lookaheadBuf[i]), std::begin(m_lookaheadBuf[i]) + m_lookaheadLength)); - m_maxLookaheadVal[i] = m_lookaheadBuf[i][m_maxLookaheadTimer[i]]; - m_maxLookaheadTimer[i] = realmod(m_maxLookaheadTimer[i] - m_lookaheadBufLoc[i], m_lookaheadLength); - } - - inputValue = m_maxLookaheadVal[i]; - } - float t = inputValue; if (t > m_yL[i])// Attack phase @@ -415,11 +356,22 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) // Keep it above the noise floor m_yL[i] = qMax(COMP_NOISE_FLOOR, m_yL[i]); + + float scVal = m_yL[i]; + + if (lookahead) + { + const float temp = scVal; + // Lookahead is calculated by picking the largest value between + // the current sidechain signal and the delayed sidechain signal. + scVal = std::max(m_scLookBuf[i][m_lookWrite], m_scLookBuf[i][(m_lookWrite + m_lookBufLength - m_lookaheadLength) % m_lookBufLength]); + m_scLookBuf[i][m_lookWrite] = temp; + } // For the visualizer - m_displayPeak[i] = qMax(m_yL[i], m_displayPeak[i]); + m_displayPeak[i] = qMax(scVal, m_displayPeak[i]); - const float currentPeakDbfs = ampToDbfs(m_yL[i]); + const float currentPeakDbfs = ampToDbfs(scVal); // Now find the gain change that should be applied, // depending on the measured input value. @@ -439,7 +391,7 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) : m_thresholdVal + (currentPeakDbfs - m_thresholdVal) * m_ratioVal; } - m_gainResult[i] = dbfsToAmp(m_gainResult[i]) / m_yL[i]; + m_gainResult[i] = dbfsToAmp(m_gainResult[i]) / scVal; m_gainResult[i] = qMax(m_rangeVal, m_gainResult[i]); } @@ -507,18 +459,10 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) // Delay the signal by 20 ms via ring buffer if lookahead is enabled if (lookahead) { - ++m_inputBufLoc; - if (m_inputBufLoc >= m_lookaheadDelayLength) - { - m_inputBufLoc = 0; - } - - const auto temp = std::array{drySignal[0], drySignal[1]}; - s[0] = m_inputBuf[0][m_inputBufLoc]; - s[1] = m_inputBuf[1][m_inputBufLoc]; - - m_inputBuf[0][m_inputBufLoc] = temp[0]; - m_inputBuf[1][m_inputBufLoc] = temp[1]; + s[0] = m_inLookBuf[0][m_lookWrite]; + s[1] = m_inLookBuf[1][m_lookWrite]; + m_inLookBuf[0][m_lookWrite] = drySignal[0]; + m_inLookBuf[1][m_lookWrite] = drySignal[1]; } else { @@ -573,6 +517,8 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) buf[f][1] = (1 - m_mixVal) * temp2 + m_mixVal * buf[f][1]; outSum += buf[f][0] * buf[f][0] + buf[f][1] * buf[f][1]; + + if (--m_lookWrite < 0) { m_lookWrite = m_lookBufLength - 1; } lInPeak = drySignal[0] > lInPeak ? drySignal[0] : lInPeak; rInPeak = drySignal[1] > rInPeak ? drySignal[1] : rInPeak; @@ -621,16 +567,13 @@ void CompressorEffect::changeSampleRate() // 200 ms m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); - // 20 ms - m_lookaheadDelayLength = 0.02 * m_sampleRate; - m_inputBuf[0].resize(m_lookaheadDelayLength); - m_inputBuf[1].resize(m_lookaheadDelayLength); - - m_lookaheadBuf[0].resize(m_lookaheadDelayLength); - m_lookaheadBuf[1].resize(m_lookaheadDelayLength); - - m_preLookaheadBuf[0].resize(m_lookaheadDelayLength); - m_preLookaheadBuf[1].resize(m_lookaheadDelayLength); + m_lookBufLength = std::ceil((20.f / 1000.f) * m_sampleRate) + 2; + for (int i = 0; i < 2; ++i) + { + m_inLookBuf[i].resize(m_lookBufLength); + m_scLookBuf[i].resize(m_lookBufLength, COMP_NOISE_FLOOR); + } + m_lookWrite = 0; calcThreshold(); calcKnee(); diff --git a/plugins/Compressor/Compressor.h b/plugins/Compressor/Compressor.h index da6ab52bcde..3fc90b7523d 100755 --- a/plugins/Compressor/Compressor.h +++ b/plugins/Compressor/Compressor.h @@ -81,14 +81,10 @@ private slots: enum class StereoLinkMode { Unlinked, Maximum, Average, Minimum, Blend }; - std::vector m_preLookaheadBuf[2]; - int m_preLookaheadBufLoc[2] = {0}; - - std::vector m_lookaheadBuf[2]; - int m_lookaheadBufLoc[2] = {0}; - - std::vector m_inputBuf[2]; - int m_inputBufLoc = 0; + std::array, 2> m_inLookBuf; + std::array, 2> m_scLookBuf; + int m_lookWrite; + int m_lookBufLength; float m_attCoeff; float m_relCoeff; @@ -99,8 +95,6 @@ private slots: int m_holdTimer[2] = {0, 0}; int m_lookaheadLength; - int m_lookaheadDelayLength; - int m_preLookaheadLength; float m_thresholdAmpVal; float m_autoMakeupVal; float m_outGainVal; diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 1516456a22a..2d04b690eed 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -58,7 +58,10 @@ CompressorControlDialog::CompressorControlDialog(CompressorControls* controls) : m_graphColor(209, 216, 228, 50), m_resetColor(200, 100, 15, 200) { - setAutoFillBackground(true); + setAutoFillBackground(false); + setAttribute(Qt::WA_OpaquePaintEvent, true); + setAttribute(Qt::WA_NoSystemBackground, true); + QPalette pal; pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); setPalette(pal); @@ -358,6 +361,7 @@ void CompressorControlDialog::lookaheadChanged() { m_lookaheadLengthKnob->setVisible(m_controls->m_lookaheadModel.value()); m_lookaheadEnabledLabel->setVisible(m_controls->m_lookaheadModel.value()); + feedbackButton->setVisible(!m_controls->m_lookaheadModel.value()); } diff --git a/plugins/Eq/EqControlsDialog.cpp b/plugins/Eq/EqControlsDialog.cpp index a26fa0db918..634bde846bc 100644 --- a/plugins/Eq/EqControlsDialog.cpp +++ b/plugins/Eq/EqControlsDialog.cpp @@ -72,17 +72,17 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : setBand( 6, &controls->m_highShelfActiveModel, &controls->m_highShelfFreqModel, &controls->m_highShelfResModel, &controls->m_highShelfGainModel, QColor(255 ,255, 255), tr( "High-shelf" ), &controls->m_highShelfPeakL, &controls->m_highShelfPeakR,0,0,0,0,0,0 ); setBand( 7, &controls->m_lpActiveModel, &controls->m_lpFreqModel, &controls->m_lpResModel, 0, QColor(255 ,255, 255), tr( "LP" ) ,0,0,0,0,0, &controls->m_lp12Model, &controls->m_lp24Model, &controls->m_lp48Model); - auto faderBg = new QPixmap(PLUGIN_NAME::getIconPixmap("faderback")); - auto faderLeds = new QPixmap(PLUGIN_NAME::getIconPixmap("faderleds")); - auto faderKnob = new QPixmap(PLUGIN_NAME::getIconPixmap("faderknob")); + static auto s_faderBg = PLUGIN_NAME::getIconPixmap("faderback"); + static auto s_faderLeds = PLUGIN_NAME::getIconPixmap("faderleds"); + static auto s_faderKnob = PLUGIN_NAME::getIconPixmap("faderknob"); - auto GainFaderIn = new EqFader(&controls->m_inGainModel, tr("Input gain"), this, faderBg, faderLeds, faderKnob, + auto GainFaderIn = new EqFader(&controls->m_inGainModel, tr("Input gain"), this, &s_faderBg, &s_faderLeds, &s_faderKnob, &controls->m_inPeakL, &controls->m_inPeakR); GainFaderIn->move( 23, 295 ); GainFaderIn->setDisplayConversion( false ); GainFaderIn->setHintText( tr( "Gain" ), "dBv"); - auto GainFaderOut = new EqFader(&controls->m_outGainModel, tr("Output gain"), this, faderBg, faderLeds, faderKnob, + auto GainFaderOut = new EqFader(&controls->m_outGainModel, tr("Output gain"), this, &s_faderBg, &s_faderLeds, &s_faderKnob, &controls->m_outPeakL, &controls->m_outPeakR); GainFaderOut->move( 453, 295); GainFaderOut->setDisplayConversion( false ); @@ -92,8 +92,8 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : int distance = 126; for( int i = 1; i < m_parameterWidget->bandCount() - 1; i++ ) { - auto gainFader = new EqFader(m_parameterWidget->getBandModels(i)->gain, tr(""), this, faderBg, faderLeds, - faderKnob, m_parameterWidget->getBandModels(i)->peakL, m_parameterWidget->getBandModels(i)->peakR); + auto gainFader = new EqFader(m_parameterWidget->getBandModels(i)->gain, tr(""), this, &s_faderBg, &s_faderLeds, + &s_faderKnob, m_parameterWidget->getBandModels(i)->peakL, m_parameterWidget->getBandModels(i)->peakR); gainFader->move( distance, 295 ); distance += 44; gainFader->setMinimumHeight(80); @@ -242,4 +242,4 @@ EqBand* EqControlsDialog::setBand(int index, BoolModel* active, FloatModel* freq } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/plugins/Eq/EqCurve.cpp b/plugins/Eq/EqCurve.cpp index 10213bfa92f..bb721a7e462 100644 --- a/plugins/Eq/EqCurve.cpp +++ b/plugins/Eq/EqCurve.cpp @@ -65,6 +65,7 @@ QRectF EqHandle::boundingRect() const float EqHandle::freqToXPixel( float freq , int w ) { + if (typeInfo::isEqual(freq, 0.0f)) { return 0.0f; } float min = log10f( 20 ); float max = log10f( 20000 ); float range = max - min; @@ -739,14 +740,18 @@ void EqCurve::paint( QPainter *painter, const QStyleOptionGraphicsItem *option, } //compute a QPainterPath m_curve = QPainterPath(); - for ( int x = 0; x < m_width ; x++ ) + //only draw the EQ curve if there are any activeHandles + if (activeHandles != 0) { - mainCurve[x] = ( ( mainCurve[x] / activeHandles ) ) - ( m_heigth/2 ); - if ( x==0 ) + for (int x = 0; x < m_width; x++) { - m_curve.moveTo( x, mainCurve[x] ); + mainCurve[x] = ((mainCurve[x] / activeHandles)) - (m_heigth / 2); + if (x == 0) + { + m_curve.moveTo(x, mainCurve[x]); + } + m_curve.lineTo(x, mainCurve[x]); } - m_curve.lineTo( x, mainCurve[x] ); } //we cache the curve painting in a pixmap for saving cpu QPixmap cacheMap( boundingRect().size().toSize() ); diff --git a/plugins/Eq/EqParameterWidget.cpp b/plugins/Eq/EqParameterWidget.cpp index b48f0f31760..ceccb669f6b 100644 --- a/plugins/Eq/EqParameterWidget.cpp +++ b/plugins/Eq/EqParameterWidget.cpp @@ -56,7 +56,7 @@ EqParameterWidget::EqParameterWidget( QWidget *parent, EqControls * controls ) : m_pixelsPerOctave = EqHandle::freqToXPixel( 10000, m_displayWidth ) - EqHandle::freqToXPixel( 5000, m_displayWidth ); //GraphicsScene and GraphicsView stuff - auto scene = new QGraphicsScene(); + auto scene = new QGraphicsScene(this); scene->setSceneRect( 0, 0, m_displayWidth, m_displayHeigth ); auto view = new QGraphicsView(this); view->setStyleSheet( "border-style: none; background: transparent;" ); @@ -65,22 +65,22 @@ EqParameterWidget::EqParameterWidget( QWidget *parent, EqControls * controls ) : view->setScene( scene ); //adds the handles - m_handleList = new QList; + m_handleList.reserve(bandCount()); for ( int i = 0; i < bandCount(); i++ ) { m_handle = new EqHandle ( i, m_displayWidth, m_displayHeigth ); - m_handleList->append( m_handle ); + m_handleList.append(m_handle); m_handle->setZValue( 1 ); scene->addItem( m_handle ); } //adds the curve widget - m_eqcurve = new EqCurve( m_handleList, m_displayWidth, m_displayHeigth ); + m_eqcurve = new EqCurve(&m_handleList, m_displayWidth, m_displayHeigth); scene->addItem( m_eqcurve ); for ( int i = 0; i < bandCount(); i++ ) { // if the data of handle position has changed update the models - QObject::connect( m_handleList->at( i ) ,SIGNAL( positionChanged() ), this ,SLOT( updateModels() ) ); + QObject::connect(m_handleList.at(i), SIGNAL(positionChanged()), this, SLOT(updateModels())); } } @@ -112,16 +112,13 @@ void EqParameterWidget::updateHandle() m_eqcurve->setModelChanged( true ); for( int i = 0 ; i < bandCount(); i++ ) { - if ( !m_handleList->at( i )->mousePressed() ) //prevents a short circuit between handle and data model + if (!m_handleList.at(i)->mousePressed()) // prevents a short circuit between handle and data model { //sets the band on active if a fader or a knob is moved bool hover = false; // prevents an action if handle is moved for ( int j = 0; j < bandCount(); j++ ) { - if ( m_handleList->at(j)->isMouseHover() ) - { - hover = true; - } + if (m_handleList.at(j)->isMouseHover()) { hover = true; } } if ( !hover ) { @@ -131,17 +128,14 @@ void EqParameterWidget::updateHandle() } changeHandle( i ); } - else - { - m_handleList->at( i )->setHandleActive( m_bands[i].active->value() ); - } + else { m_handleList.at(i)->setHandleActive(m_bands[i].active->value()); } } - if ( m_bands[0].hp12->value() ) m_handleList->at( 0 )->sethp12(); - if ( m_bands[0].hp24->value() ) m_handleList->at( 0 )->sethp24(); - if ( m_bands[0].hp48->value() ) m_handleList->at( 0 )->sethp48(); - if ( m_bands[7].lp12->value() ) m_handleList->at( 7 )->setlp12(); - if ( m_bands[7].lp24->value() ) m_handleList->at( 7 )->setlp24(); - if ( m_bands[7].lp48->value() ) m_handleList->at( 7 )->setlp48(); + if (m_bands[0].hp12->value()) m_handleList.at(0)->sethp12(); + if (m_bands[0].hp24->value()) m_handleList.at(0)->sethp24(); + if (m_bands[0].hp48->value()) m_handleList.at(0)->sethp48(); + if (m_bands[7].lp12->value()) m_handleList.at(7)->setlp12(); + if (m_bands[7].lp24->value()) m_handleList.at(7)->setlp24(); + if (m_bands[7].lp48->value()) m_handleList.at(7)->setlp48(); } @@ -151,7 +145,7 @@ void EqParameterWidget::changeHandle( int i ) { //fill x, y, and bw with data from model float x = EqHandle::freqToXPixel( m_bands[i].freq->value(), m_displayWidth ); - float y = m_handleList->at( i )->y(); + float y = m_handleList.at(i)->y(); //for pass filters there is no gain model if( m_bands[i].gain ) { @@ -164,48 +158,45 @@ void EqParameterWidget::changeHandle( int i ) switch ( i ) { case 0 : - m_handleList->at( i )->setType( EqHandleType::HighPass ); - m_handleList->at( i )->setPos( x, m_displayHeigth / 2 ); + m_handleList.at(i)->setType(EqHandleType::HighPass); + m_handleList.at(i)->setPos(x, m_displayHeigth / 2); break; case 1: - m_handleList->at( i )->setType( EqHandleType::LowShelf ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::LowShelf); + m_handleList.at(i)->setPos(x, y); break; case 2: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 3: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 4: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 5: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 6: - m_handleList->at( i )->setType( EqHandleType::HighShelf ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::HighShelf); + m_handleList.at(i)->setPos(x, y); break; case 7: - m_handleList->at( i )->setType( EqHandleType::LowPass ); - m_handleList->at( i )->setPos( QPointF( x, m_displayHeigth / 2 ) ); + m_handleList.at(i)->setType(EqHandleType::LowPass); + m_handleList.at(i)->setPos(QPointF(x, m_displayHeigth / 2)); break; } // set resonance/bandwidth for each handle - if ( m_handleList->at( i )->getResonance() != bw ) - { - m_handleList->at( i )->setResonance( bw ); - } + if (m_handleList.at(i)->getResonance() != bw) { m_handleList.at(i)->setResonance(bw); } // and the active status - m_handleList->at( i )->setHandleActive( m_bands[i].active->value() ); - m_handleList->at( i )->update(); + m_handleList.at(i)->setHandleActive(m_bands[i].active->value()); + m_handleList.at(i)->update(); m_eqcurve->update(); } @@ -216,19 +207,17 @@ void EqParameterWidget::updateModels() { for ( int i=0 ; i < bandCount(); i++ ) { - m_bands[i].freq->setValue( EqHandle::xPixelToFreq( m_handleList->at( i )->x(), m_displayWidth ) ); + m_bands[i].freq->setValue(EqHandle::xPixelToFreq(m_handleList.at(i)->x(), m_displayWidth)); if( m_bands[i].gain ) { - m_bands[i].gain->setValue( EqHandle::yPixelToGain( m_handleList->at(i)->y(), m_displayHeigth, m_pixelsPerUnitHeight ) ); + m_bands[i].gain->setValue( + EqHandle::yPixelToGain(m_handleList.at(i)->y(), m_displayHeigth, m_pixelsPerUnitHeight)); } - m_bands[i].res->setValue( m_handleList->at( i )->getResonance() ); + m_bands[i].res->setValue(m_handleList.at(i)->getResonance()); //identifies the handle which is moved and set the band active - if ( sender() == m_handleList->at( i ) ) - { - m_bands[i].active->setValue( true ); - } + if (sender() == m_handleList.at(i)) { m_bands[i].active->setValue(true); } } m_eqcurve->update(); } diff --git a/plugins/Eq/EqParameterWidget.h b/plugins/Eq/EqParameterWidget.h index f80499395c7..c3444873bdd 100644 --- a/plugins/Eq/EqParameterWidget.h +++ b/plugins/Eq/EqParameterWidget.h @@ -75,7 +75,7 @@ class EqParameterWidget : public QWidget public: explicit EqParameterWidget( QWidget *parent = 0, EqControls * controls = 0 ); ~EqParameterWidget() override; - QList *m_handleList; + QList m_handleList; const int bandCount() { diff --git a/plugins/GigPlayer/CMakeLists.txt b/plugins/GigPlayer/CMakeLists.txt index 24db813bdee..7b634b605ce 100644 --- a/plugins/GigPlayer/CMakeLists.txt +++ b/plugins/GigPlayer/CMakeLists.txt @@ -12,8 +12,13 @@ if(LMMS_HAVE_GIG) add_definitions(${GCC_GIG_COMPILE_FLAGS}) endif(LMMS_BUILD_WIN32) - LINK_DIRECTORIES(${GIG_LIBRARY_DIRS} ${SAMPLERATE_LIBRARY_DIRS}) - LINK_LIBRARIES(${GIG_LIBRARIES} ${SAMPLERATE_LIBRARIES}) - BUILD_PLUGIN(gigplayer GigPlayer.cpp GigPlayer.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES GigPlayer.h PatchesDialog.h UICFILES PatchesDialog.ui EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") + link_directories(${GIG_LIBRARY_DIRS}) + link_libraries(${GIG_LIBRARIES}) + build_plugin(gigplayer + GigPlayer.cpp GigPlayer.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui + MOCFILES GigPlayer.h PatchesDialog.h + UICFILES PatchesDialog.ui + EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" + ) + target_link_libraries(gigplayer SampleRate::samplerate) endif(LMMS_HAVE_GIG) - diff --git a/plugins/LOMM/CMakeLists.txt b/plugins/LOMM/CMakeLists.txt new file mode 100644 index 00000000000..dd6178f032a --- /dev/null +++ b/plugins/LOMM/CMakeLists.txt @@ -0,0 +1,3 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(lomm LOMM.cpp LOMMControls.cpp LOMMControlDialog.cpp MOCFILES LOMM.h LOMMControls.h LOMMControlDialog.h EMBEDDED_RESOURCES *.png) diff --git a/plugins/LOMM/LOMM.cpp b/plugins/LOMM/LOMM.cpp new file mode 100644 index 00000000000..6dc640626b3 --- /dev/null +++ b/plugins/LOMM/LOMM.cpp @@ -0,0 +1,444 @@ +/* + * LOMM.cpp + * + * Copyright (c) 2023 Lost Robot + * + * 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 "LOMM.h" + +#include "embed.h" +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ + Plugin::Descriptor PLUGIN_EXPORT lomm_plugin_descriptor = + { + LMMS_STRINGIFY(PLUGIN_NAME), + "LOMM", + QT_TRANSLATE_NOOP("PluginBrowser", "Upwards/downwards multiband compression plugin powered by the eldritch elder god LOMMUS."), + "Lost Robot ", + 0x0100, + Plugin::Type::Effect, + new PluginPixmapLoader("logo"), + nullptr, + nullptr + }; +} + + +LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&lomm_plugin_descriptor, parent, key), + m_lommControls(this), + m_sampleRate(Engine::audioEngine()->processingSampleRate()), + m_lp1(m_sampleRate), + m_lp2(m_sampleRate), + m_hp1(m_sampleRate), + m_hp2(m_sampleRate), + m_needsUpdate(true), + m_coeffPrecalc(-0.05), + m_crestTimeConst(0.999), + m_lookWrite(0), + m_lookBufLength(2) +{ + autoQuitModel()->setValue(autoQuitModel()->maxValue()); + + m_yL[0][0] = m_yL[0][1] = LOMM_MIN_FLOOR; + m_yL[1][0] = m_yL[1][1] = LOMM_MIN_FLOOR; + m_yL[2][0] = m_yL[2][1] = LOMM_MIN_FLOOR; + + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); + emit changeSampleRate(); +} + +void LOMMEffect::changeSampleRate() +{ + m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_lp1.setSampleRate(m_sampleRate); + m_lp2.setSampleRate(m_sampleRate); + m_hp1.setSampleRate(m_sampleRate); + m_hp2.setSampleRate(m_sampleRate); + + m_coeffPrecalc = -2.2f / (m_sampleRate * 0.001f); + m_needsUpdate = true; + + m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); + + m_lookBufLength = std::ceil((LOMM_MAX_LOOKAHEAD / 1000.f) * m_sampleRate) + 2; + for (int i = 0; i < 2; ++i) + { + for (int j = 0; j < 3; ++j) + { + m_inLookBuf[j][i].resize(m_lookBufLength); + m_scLookBuf[j][i].resize(m_lookBufLength, LOMM_MIN_FLOOR); + } + } +} + +void LOMMEffect::clearFilterHistories() +{ + m_lp1.clearHistory(); + m_lp2.clearHistory(); + m_hp1.clearHistory(); + m_hp2.clearHistory(); +} + + + + +bool LOMMEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) +{ + if (!isEnabled() || !isRunning()) + { + return false; + } + + if (m_needsUpdate || m_lommControls.m_split1Model.isValueChanged()) + { + m_lp1.setLowpass(m_lommControls.m_split1Model.value()); + m_hp1.setHighpass(m_lommControls.m_split1Model.value()); + } + if (m_needsUpdate || m_lommControls.m_split2Model.isValueChanged()) + { + m_lp2.setLowpass(m_lommControls.m_split2Model.value()); + m_hp2.setHighpass(m_lommControls.m_split2Model.value()); + } + m_needsUpdate = false; + + float outSum = 0.f; + const float d = dryLevel(); + const float w = wetLevel(); + + const float depth = m_lommControls.m_depthModel.value(); + const float time = m_lommControls.m_timeModel.value(); + const float inVol = dbfsToAmp(m_lommControls.m_inVolModel.value()); + const float outVol = dbfsToAmp(m_lommControls.m_outVolModel.value()); + const float upward = m_lommControls.m_upwardModel.value(); + const float downward = m_lommControls.m_downwardModel.value(); + const bool split1Enabled = m_lommControls.m_split1EnabledModel.value(); + const bool split2Enabled = m_lommControls.m_split2EnabledModel.value(); + const bool band1Enabled = m_lommControls.m_band1EnabledModel.value(); + const bool band2Enabled = m_lommControls.m_band2EnabledModel.value(); + const bool band3Enabled = m_lommControls.m_band3EnabledModel.value(); + const float inHigh = dbfsToAmp(m_lommControls.m_inHighModel.value()); + const float inMid = dbfsToAmp(m_lommControls.m_inMidModel.value()); + const float inLow = dbfsToAmp(m_lommControls.m_inLowModel.value()); + float inBandVol[3] = {inHigh, inMid, inLow}; + const float outHigh = dbfsToAmp(m_lommControls.m_outHighModel.value()); + const float outMid = dbfsToAmp(m_lommControls.m_outMidModel.value()); + const float outLow = dbfsToAmp(m_lommControls.m_outLowModel.value()); + float outBandVol[3] = {outHigh, outMid, outLow}; + const float aThreshH = m_lommControls.m_aThreshHModel.value(); + const float aThreshM = m_lommControls.m_aThreshMModel.value(); + const float aThreshL = m_lommControls.m_aThreshLModel.value(); + float aThresh[3] = {aThreshH, aThreshM, aThreshL}; + const float aRatioH = m_lommControls.m_aRatioHModel.value(); + const float aRatioM = m_lommControls.m_aRatioMModel.value(); + const float aRatioL = m_lommControls.m_aRatioLModel.value(); + float aRatio[3] = {1.f / aRatioH, 1.f / aRatioM, 1.f / aRatioL}; + const float bThreshH = m_lommControls.m_bThreshHModel.value(); + const float bThreshM = m_lommControls.m_bThreshMModel.value(); + const float bThreshL = m_lommControls.m_bThreshLModel.value(); + float bThresh[3] = {bThreshH, bThreshM, bThreshL}; + const float bRatioH = m_lommControls.m_bRatioHModel.value(); + const float bRatioM = m_lommControls.m_bRatioMModel.value(); + const float bRatioL = m_lommControls.m_bRatioLModel.value(); + float bRatio[3] = {1.f / bRatioH, 1.f / bRatioM, 1.f / bRatioL}; + const float atkH = m_lommControls.m_atkHModel.value() * time; + const float atkM = m_lommControls.m_atkMModel.value() * time; + const float atkL = m_lommControls.m_atkLModel.value() * time; + const float atkCoefH = msToCoeff(atkH); + const float atkCoefM = msToCoeff(atkM); + const float atkCoefL = msToCoeff(atkL); + float atk[3] = {atkH, atkM, atkL}; + float atkCoef[3] = {atkCoefH, atkCoefM, atkCoefL}; + const float relH = m_lommControls.m_relHModel.value() * time; + const float relM = m_lommControls.m_relMModel.value() * time; + const float relL = m_lommControls.m_relLModel.value() * time; + const float relCoefH = msToCoeff(relH); + const float relCoefM = msToCoeff(relM); + const float relCoefL = msToCoeff(relL); + float rel[3] = {relH, relM, relL}; + float relCoef[3] = {relCoefH, relCoefM, relCoefL}; + const float rmsTime = m_lommControls.m_rmsTimeModel.value(); + const float rmsTimeConst = (rmsTime == 0) ? 0 : exp(-1.f / (rmsTime * 0.001f * m_sampleRate)); + const float knee = m_lommControls.m_kneeModel.value() * 0.5f; + const float range = m_lommControls.m_rangeModel.value(); + const float rangeAmp = dbfsToAmp(range); + const float balance = m_lommControls.m_balanceModel.value(); + const float balanceAmpTemp = dbfsToAmp(balance); + const float balanceAmp[2] = {1.f / balanceAmpTemp, balanceAmpTemp}; + const bool depthScaling = m_lommControls.m_depthScalingModel.value(); + const bool stereoLink = m_lommControls.m_stereoLinkModel.value(); + const float autoTime = m_lommControls.m_autoTimeModel.value() * m_lommControls.m_autoTimeModel.value(); + const float mix = m_lommControls.m_mixModel.value(); + const bool midside = m_lommControls.m_midsideModel.value(); + const bool lookaheadEnable = m_lommControls.m_lookaheadEnableModel.value(); + const int lookahead = std::ceil((m_lommControls.m_lookaheadModel.value() / 1000.f) * m_sampleRate); + const bool feedback = m_lommControls.m_feedbackModel.value() && !lookaheadEnable; + const bool lowSideUpwardSuppress = m_lommControls.m_lowSideUpwardSuppressModel.value() && midside; + + for (fpp_t f = 0; f < frames; ++f) + { + std::array s = {buf[f][0], buf[f][1]}; + + // Convert left/right to mid/side. Side channel is intentionally made + // to be 6 dB louder to bring it into volume ranges comparable to the mid channel. + if (midside) + { + float tempS0 = s[0]; + s[0] = (s[0] + s[1]) * 0.5f; + s[1] = tempS0 - s[1]; + } + + std::array, 3> bands = {{}}; + std::array, 3> bandsDry = {{}}; + + for (int i = 0; i < 2; ++i)// Channels + { + // These values are for the Auto time knob. Higher crest factor allows for faster attack/release. + float inSquared = s[i] * s[i]; + m_crestPeakVal[i] = std::max(std::max(LOMM_MIN_FLOOR, inSquared), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inSquared)); + m_crestRmsVal[i] = std::max(LOMM_MIN_FLOOR, m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inSquared))); + m_crestFactorVal[i] = m_crestPeakVal[i] / m_crestRmsVal[i]; + float crestFactorValTemp = ((m_crestFactorVal[i] - LOMM_AUTO_TIME_ADJUST) * autoTime) + LOMM_AUTO_TIME_ADJUST; + + // Crossover filters + bands[0][i] = m_hp1.update(s[i], i); + bands[1][i] = m_hp2.update(m_lp1.update(s[i], i), i); + bands[2][i] = m_lp2.update(s[i], i); + + if (!split1Enabled) + { + bands[1][i] += bands[0][i]; + bands[0][i] = 0; + } + if (!split2Enabled) + { + bands[1][i] += bands[2][i]; + bands[2][i] = 0; + } + + // Mute disabled bands + bands[0][i] *= band1Enabled; + bands[1][i] *= band2Enabled; + bands[2][i] *= band3Enabled; + + std::array detect = {0, 0, 0}; + for (int j = 0; j < 3; ++j)// Bands + { + bandsDry[j][i] = bands[j][i]; + + if (feedback && !lookaheadEnable) + { + bands[j][i] = m_prevOut[j][i]; + } + + bands[j][i] *= inBandVol[j] * inVol * balanceAmp[i]; + + if (rmsTime > 0)// RMS + { + m_rms[j][i] = rmsTimeConst * m_rms[j][i] + ((1 - rmsTimeConst) * (bands[j][i] * bands[j][i])); + detect[j] = std::max(LOMM_MIN_FLOOR, std::sqrt(m_rms[j][i])); + } + else// Peak + { + detect[j] = std::max(LOMM_MIN_FLOOR, std::abs(bands[j][i])); + } + + if (detect[j] > m_yL[j][i])// Attack phase + { + // Calculate attack value depending on crest factor + const float currentAttack = autoTime + ? msToCoeff(LOMM_AUTO_TIME_ADJUST * atk[j] / crestFactorValTemp) + : atkCoef[j]; + + m_yL[j][i] = m_yL[j][i] * currentAttack + (1 - currentAttack) * detect[j]; + } + else// Release phase + { + // Calculate release value depending on crest factor + const float currentRelease = autoTime + ? msToCoeff(LOMM_AUTO_TIME_ADJUST * rel[j] / crestFactorValTemp) + : relCoef[j]; + + m_yL[j][i] = m_yL[j][i] * currentRelease + (1 - currentRelease) * detect[j]; + } + + m_yL[j][i] = std::max(LOMM_MIN_FLOOR, m_yL[j][i]); + + float yAmp = m_yL[j][i]; + if (lookaheadEnable) + { + float temp = yAmp; + // Lookahead is calculated by picking the largest value between + // the current sidechain signal and the delayed sidechain signal. + yAmp = std::max(m_scLookBuf[j][i][m_lookWrite], m_scLookBuf[j][i][(m_lookWrite + m_lookBufLength - lookahead) % m_lookBufLength]); + m_scLookBuf[j][i][m_lookWrite] = temp; + } + + const float yDbfs = ampToDbfs(yAmp); + + float aboveGain = 0; + float belowGain = 0; + + // Downward compression + if (yDbfs - aThresh[j] < -knee)// Below knee + { + aboveGain = yDbfs; + } + else if (yDbfs - aThresh[j] < knee)// Within knee + { + const float temp = yDbfs - aThresh[j] + knee; + aboveGain = yDbfs + (aRatio[j] - 1) * temp * temp / (4 * knee); + } + else// Above knee + { + aboveGain = aThresh[j] + (yDbfs - aThresh[j]) * aRatio[j]; + } + if (aboveGain < yDbfs) + { + if (downward * depth <= 1) + { + aboveGain = linearInterpolate(yDbfs, aboveGain, downward * depth); + } + else + { + aboveGain = linearInterpolate(aboveGain, aThresh[j], downward * depth - 1); + } + } + + // Upward compression + if (yDbfs - bThresh[j] > knee)// Above knee + { + belowGain = yDbfs; + } + else if (bThresh[j] - yDbfs < knee)// Within knee + { + const float temp = bThresh[j] - yDbfs + knee; + belowGain = yDbfs + (1 - bRatio[j]) * temp * temp / (4 * knee); + } + else// Below knee + { + belowGain = bThresh[j] + (yDbfs - bThresh[j]) * bRatio[j]; + } + if (belowGain > yDbfs) + { + if (upward * depth <= 1) + { + belowGain = linearInterpolate(yDbfs, belowGain, upward * depth); + } + else + { + belowGain = linearInterpolate(belowGain, bThresh[j], upward * depth - 1); + } + } + + m_displayIn[j][i] = yDbfs; + m_gainResult[j][i] = (dbfsToAmp(aboveGain) / yAmp) * (dbfsToAmp(belowGain) / yAmp); + if (lowSideUpwardSuppress && m_gainResult[j][i] > 1 && j == 2 && i == 1) //undo upward compression if low side band + { + m_gainResult[j][i] = 1; + } + m_gainResult[j][i] = std::min(m_gainResult[j][i], rangeAmp); + m_displayOut[j][i] = ampToDbfs(std::max(LOMM_MIN_FLOOR, yAmp * m_gainResult[j][i])); + + // Apply the same gain reduction to both channels if stereo link is enabled. + if (stereoLink && i == 1) + { + if (m_gainResult[j][1] < m_gainResult[j][0]) + { + m_gainResult[j][0] = m_gainResult[j][1]; + m_displayOut[j][0] = m_displayIn[j][0] - (m_displayIn[j][1] - m_displayOut[j][1]); + } + else + { + m_gainResult[j][1] = m_gainResult[j][0]; + m_displayOut[j][1] = m_displayIn[j][1] - (m_displayIn[j][0] - m_displayOut[j][0]); + } + } + } + } + + for (int i = 0; i < 2; ++i)// Channels + { + for (int j = 0; j < 3; ++j)// Bands + { + if (lookaheadEnable) + { + float temp = bands[j][i]; + bands[j][i] = m_inLookBuf[j][i][m_lookWrite]; + m_inLookBuf[j][i][m_lookWrite] = temp; + bandsDry[j][i] = bands[j][i]; + } + else if (feedback) + { + bands[j][i] = bandsDry[j][i] * inBandVol[j] * inVol * balanceAmp[i]; + } + + // Apply gain reduction + bands[j][i] *= m_gainResult[j][i]; + + // Store for Feedback + m_prevOut[j][i] = bands[j][i]; + + bands[j][i] *= outBandVol[j]; + + bands[j][i] = linearInterpolate(bandsDry[j][i], bands[j][i], mix); + } + + s[i] = bands[0][i] + bands[1][i] + bands[2][i]; + + s[i] *= linearInterpolate(1.f, outVol, mix * (depthScaling ? depth : 1)); + } + + // Convert mid/side back to left/right. + // Note that the side channel was intentionally made to be 6 dB louder prior to compression. + if (midside) + { + float tempS0 = s[0]; + s[0] = s[0] + (s[1] * 0.5f); + s[1] = tempS0 - (s[1] * 0.5f); + } + + if (--m_lookWrite < 0) { m_lookWrite = m_lookBufLength - 1; } + + buf[f][0] = d * buf[f][0] + w * s[0]; + buf[f][1] = d * buf[f][1] + w * s[1]; + outSum += buf[f][0] + buf[f][1]; + } + + checkGate(outSum / frames); + return isRunning(); +} + +extern "C" +{ + // necessary for getting instance out of shared lib + PLUGIN_EXPORT Plugin * lmms_plugin_main(Model* parent, void* data) + { + return new LOMMEffect(parent, static_cast(data)); + } +} + +} // namespace lmms diff --git a/plugins/LOMM/LOMM.h b/plugins/LOMM/LOMM.h new file mode 100644 index 00000000000..039f80b6a63 --- /dev/null +++ b/plugins/LOMM/LOMM.h @@ -0,0 +1,106 @@ +/* + * LOMM.h + * + * Copyright (c) 2023 Lost Robot + * + * 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_LOMM_H +#define LMMS_LOMM_H + +#include "LOMMControls.h" +#include "Effect.h" + +#include "BasicFilters.h" +#include "lmms_math.h" + +namespace lmms +{ + +constexpr inline float LOMM_MIN_FLOOR = 0.00012589;// -72 dBFS +constexpr inline float LOMM_MAX_LOOKAHEAD = 20.f; +constexpr inline float LOMM_AUTO_TIME_ADJUST = 5.f; + +class LOMMEffect : public Effect +{ + Q_OBJECT +public: + LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); + ~LOMMEffect() override = default; + bool processAudioBuffer(sampleFrame* buf, const fpp_t frames) override; + + EffectControls* controls() override + { + return &m_lommControls; + } + + void clearFilterHistories(); + + inline float msToCoeff(float ms) + { + return (ms == 0) ? 0 : exp(m_coeffPrecalc / ms); + } + +private slots: + void changeSampleRate(); + +private: + LOMMControls m_lommControls; + + float m_sampleRate; + + StereoLinkwitzRiley m_lp1; + StereoLinkwitzRiley m_lp2; + + StereoLinkwitzRiley m_hp1; + StereoLinkwitzRiley m_hp2; + + bool m_needsUpdate; + float m_coeffPrecalc; + + std::array, 3> m_yL; + std::array, 3> m_rms; + std::array, 3> m_gainResult; + + std::array, 3> m_displayIn; + std::array, 3> m_displayOut; + + std::array m_crestPeakVal; + std::array m_crestRmsVal; + std::array m_crestFactorVal; + float m_crestTimeConst = 0.0f; + + std::array, 3> m_prevOut; + + std::array, 2>, 3> m_inLookBuf; + std::array, 2>, 3> m_scLookBuf; + + int m_lookWrite = 0; + int m_lookBufLength = 0; + + friend class LOMMControls; + friend class gui::LOMMControlDialog; +}; + + +} // namespace lmms + +#endif // LMMS_LOMM_H diff --git a/plugins/LOMM/LOMMControlDialog.cpp b/plugins/LOMM/LOMMControlDialog.cpp new file mode 100644 index 00000000000..e53987a05de --- /dev/null +++ b/plugins/LOMM/LOMMControlDialog.cpp @@ -0,0 +1,275 @@ +/* + * LOMMControlDialog.cpp + * + * Copyright (c) 2023 Lost Robot + * + * 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 "LOMM.h" +#include "LOMMControlDialog.h" +#include "LOMMControls.h" + + +namespace lmms::gui +{ + +LOMMControlDialog::LOMMControlDialog(LOMMControls* controls) : + EffectControlDialog(controls), + m_controls(controls) +{ + setAutoFillBackground(true); + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(400, 256); + + createKnob(KnobType::Bright26, this, 10, 4, &controls->m_depthModel, tr("Depth:"), "", tr("Compression amount for all bands")); + createKnob(KnobType::Bright26, this, 10, 41, &controls->m_timeModel, tr("Time:"), "", tr("Attack/release scaling for all bands")); + createKnob(KnobType::Bright26, this, 10, 220, &controls->m_inVolModel, tr("Input Volume:"), " dB", tr("Input volume")); + createKnob(KnobType::Bright26, this, 363, 220, &controls->m_outVolModel, tr("Output Volume:"), " dB", tr("Output volume")); + createKnob(KnobType::Bright26, this, 10, 179, &controls->m_upwardModel, tr("Upward Depth:"), "", tr("Upward compression amount for all bands")); + createKnob(KnobType::Bright26, this, 363, 179, &controls->m_downwardModel, tr("Downward Depth:"), "", tr("Downward compression amount for all bands")); + + createLcdFloatSpinBox(5, 2, "11green", tr("High/Mid Crossover"), this, 352, 76, &controls->m_split1Model, tr("High/Mid Crossover")); + createLcdFloatSpinBox(5, 2, "11green", tr("Mid/Low Crossover"), this, 352, 156, &controls->m_split2Model, tr("Mid/Low Crossover")); + + createPixmapButton(tr("High/mid band split"), this, 369, 104, &controls->m_split1EnabledModel, "crossover_led_green", "crossover_led_off", tr("High/mid band split")); + createPixmapButton(tr("Mid/low band split"), this, 369, 126, &controls->m_split2EnabledModel, "crossover_led_green", "crossover_led_off", tr("Mid/low band split")); + + createPixmapButton(tr("Enable High Band"), this, 143, 66, &controls->m_band1EnabledModel, "high_band_active", "high_band_inactive", tr("Enable High Band")); + createPixmapButton(tr("Enable Mid Band"), this, 143, 146, &controls->m_band2EnabledModel, "mid_band_active", "mid_band_inactive", tr("Enable Mid Band")); + createPixmapButton(tr("Enable Low Band"), this, 143, 226, &controls->m_band3EnabledModel, "low_band_active", "low_band_inactive", tr("Enable Low Band")); + + createKnob(KnobType::Bright26, this, 53, 43, &controls->m_inHighModel, tr("High Input Volume:"), " dB", tr("Input volume for high band")); + createKnob(KnobType::Bright26, this, 53, 123, &controls->m_inMidModel, tr("Mid Input Volume:"), " dB", tr("Input volume for mid band")); + createKnob(KnobType::Bright26, this, 53, 203, &controls->m_inLowModel, tr("Low Input Volume:"), " dB", tr("Input volume for low band")); + createKnob(KnobType::Bright26, this, 320, 43, &controls->m_outHighModel, tr("High Output Volume:"), " dB", tr("Output volume for high band")); + createKnob(KnobType::Bright26, this, 320, 123, &controls->m_outMidModel, tr("Mid Output Volume:"), " dB", tr("Output volume for mid band")); + createKnob(KnobType::Bright26, this, 320, 203, &controls->m_outLowModel, tr("Low Output Volume:"), " dB", tr("Output volume for low band")); + + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold High"), this, 300, 13, &controls->m_aThreshHModel, tr("Downward compression threshold for high band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold Mid"), this, 300, 93, &controls->m_aThreshMModel, tr("Downward compression threshold for mid band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold Low"), this, 300, 173, &controls->m_aThreshLModel, tr("Downward compression threshold for low band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio High"), this, 284, 44, &controls->m_aRatioHModel, tr("Downward compression ratio for high band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio Mid"), this, 284, 124, &controls->m_aRatioMModel, tr("Downward compression ratio for mid band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio Low"), this, 284, 204, &controls->m_aRatioLModel, tr("Downward compression ratio for low band")); + + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold High"), this, 59, 13, &controls->m_bThreshHModel, tr("Upward compression threshold for high band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold Mid"), this, 59, 93, &controls->m_bThreshMModel, tr("Upward compression threshold for mid band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold Low"), this, 59, 173, &controls->m_bThreshLModel, tr("Upward compression threshold for low band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio High"), this, 87, 44, &controls->m_bRatioHModel, tr("Upward compression ratio for high band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio Mid"), this, 87, 124, &controls->m_bRatioMModel, tr("Upward compression ratio for mid band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio Low"), this, 87, 204, &controls->m_bRatioLModel, tr("Upward compression ratio for low band")); + + createKnob(KnobType::Small17, this, 120, 61, &controls->m_atkHModel, tr("Attack High:"), " ms", tr("Attack time for high band")); + createKnob(KnobType::Small17, this, 120, 141, &controls->m_atkMModel, tr("Attack Mid:"), " ms", tr("Attack time for mid band")); + createKnob(KnobType::Small17, this, 120, 221, &controls->m_atkLModel, tr("Attack Low:"), " ms", tr("Attack time for low band")); + createKnob(KnobType::Small17, this, 261, 61, &controls->m_relHModel, tr("Release High:"), " ms", tr("Release time for high band")); + createKnob(KnobType::Small17, this, 261, 141, &controls->m_relMModel, tr("Release Mid:"), " ms", tr("Release time for mid band")); + createKnob(KnobType::Small17, this, 261, 221, &controls->m_relLModel, tr("Release Low:"), " ms", tr("Release time for low band")); + + createKnob(KnobType::Small17, this, 380, 42, &controls->m_rmsTimeModel, tr("RMS Time:"), " ms", tr("RMS size for sidechain signal (set to 0 for Peak mode)")); + createKnob(KnobType::Small17, this, 356, 42, &controls->m_kneeModel, tr("Knee:"), " dB", tr("Knee size for all compressors")); + createKnob(KnobType::Small17, this, 24, 146, &controls->m_rangeModel, tr("Range:"), " dB", tr("Maximum gain increase for all bands")); + createKnob(KnobType::Small17, this, 13, 114, &controls->m_balanceModel, tr("Balance:"), " dB", tr("Bias input volume towards one channel")); + + createPixmapButton(tr("Scale output volume with Depth"), this, 51, 0, &controls->m_depthScalingModel, "depthScaling_active", "depthScaling_inactive", + tr("Scale output volume with Depth parameter")); + createPixmapButton(tr("Stereo Link"), this, 52, 237, &controls->m_stereoLinkModel, "stereoLink_active", "stereoLink_inactive", + tr("Apply same gain change to both channels")); + + createKnob(KnobType::Small17, this, 24, 80, &controls->m_autoTimeModel, tr("Auto Time:"), "", tr("Speed up attack and release times when transients occur")); + createKnob(KnobType::Bright26, this, 363, 4, &controls->m_mixModel, tr("Mix:"), "", tr("Wet/Dry of all bands")); + + m_feedbackButton = createPixmapButton(tr("Feedback"), this, 317, 238, &controls->m_feedbackModel, "feedback_active", "feedback_inactive", + tr("Use output as sidechain signal instead of input")); + createPixmapButton(tr("Mid/Side"), this, 285, 238, &controls->m_midsideModel, "midside_active", "midside_inactive", tr("Compress mid/side channels instead of left/right")); + m_lowSideUpwardSuppressButton = createPixmapButton(tr("Suppress upward compression for side band"), this, 106, 180, &controls->m_lowSideUpwardSuppressModel, + "lowSideUpwardSuppress_active", "lowSideUpwardSuppress_inactive", tr("Suppress upward compression for side band")); + createPixmapButton(tr("Lookahead"), this, 147, 0, &controls->m_lookaheadEnableModel, "lookahead_active", "lookahead_inactive", + tr(("Enable lookahead with fixed " + std::to_string(int(LOMM_MAX_LOOKAHEAD)) + " ms latency").c_str())); + createLcdFloatSpinBox(2, 2, "11green", tr("Lookahead"), this, 214, 2, &controls->m_lookaheadModel, tr("Lookahead length")); + + PixmapButton* initButton = createPixmapButton(tr("Clear all parameters"), this, 84, 237, nullptr, "init_active", "init_inactive", tr("Clear all parameters")); + + connect(initButton, SIGNAL(clicked()), m_controls, SLOT(resetAllParameters())); + connect(&controls->m_lookaheadEnableModel, SIGNAL(dataChanged()), this, SLOT(updateFeedbackVisibility())); + connect(&controls->m_midsideModel, SIGNAL(dataChanged()), this, SLOT(updateLowSideUpwardSuppressVisibility())); + connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(updateDisplay())); + + emit updateFeedbackVisibility(); + emit updateLowSideUpwardSuppressVisibility(); +} + +void LOMMControlDialog::updateFeedbackVisibility() +{ + m_feedbackButton->setVisible(!m_controls->m_lookaheadEnableModel.value()); +} + +void LOMMControlDialog::updateLowSideUpwardSuppressVisibility() +{ + m_lowSideUpwardSuppressButton->setVisible(m_controls->m_midsideModel.value()); +} + +void LOMMControlDialog::updateDisplay() +{ + update(); +} + +void LOMMControlDialog::paintEvent(QPaintEvent *event) +{ + if (!isVisible()) { return; } + + QPainter p; + p.begin(this); + + // Draw threshold lines + QColor aColor(255, 255, 0, 31); + QColor bColor(255, 0, 0, 31); + QPen aPen(QColor(255, 255, 0, 255), 1); + QPen bPen(QColor(255, 0, 0, 255), 1); + int thresholdsX[] = {dbfsToX(m_controls->m_aThreshHModel.value()), + dbfsToX(m_controls->m_aThreshMModel.value()), + dbfsToX(m_controls->m_aThreshLModel.value()), + dbfsToX(m_controls->m_bThreshHModel.value()), + dbfsToX(m_controls->m_bThreshMModel.value()), + dbfsToX(m_controls->m_bThreshLModel.value())}; + for (int i = 0; i < 3; ++i) { + p.setPen(aPen); + p.fillRect(thresholdsX[i], LOMM_DISPLAY_Y[2 * i], LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH - thresholdsX[i], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - LOMM_DISPLAY_Y[2 * i], aColor); + p.drawLine(thresholdsX[i], LOMM_DISPLAY_Y[2 * i], thresholdsX[i], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + + p.setPen(bPen); + p.fillRect(LOMM_DISPLAY_X, LOMM_DISPLAY_Y[2 * i], thresholdsX[i + 3] - LOMM_DISPLAY_X, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - LOMM_DISPLAY_Y[2 * i], bColor); + p.drawLine(thresholdsX[i + 3], LOMM_DISPLAY_Y[2 * i], thresholdsX[i + 3], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + } + + QPen inputPen(QColor(200, 200, 200, 80), 1); + QPen outputPen(QColor(255, 255, 255, 255), 1); + for (int i = 0; i < 3; ++i) { + // Draw input lines + p.setPen(inputPen); + int inL = dbfsToX(m_controls->m_effect->m_displayIn[i][0]); + p.drawLine(inL, LOMM_DISPLAY_Y[2 * i] + 4, inL, LOMM_DISPLAY_Y[2 * i] + LOMM_DISPLAY_HEIGHT); + int inR = dbfsToX(m_controls->m_effect->m_displayIn[i][1]); + p.drawLine(inR, LOMM_DISPLAY_Y[2 * i + 1], inR, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - 4); + + // Draw output lines + p.setPen(outputPen); + int outL = dbfsToX(m_controls->m_effect->m_displayOut[i][0]); + p.drawLine(outL, LOMM_DISPLAY_Y[2 * i], outL, LOMM_DISPLAY_Y[2 * i] + LOMM_DISPLAY_HEIGHT); + int outR = dbfsToX(m_controls->m_effect->m_displayOut[i][1]); + p.drawLine(outR, LOMM_DISPLAY_Y[2 * i + 1], outR, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + } + + p.end(); +} + +int LOMMControlDialog::dbfsToX(float dbfs) +{ + float returnX = (dbfs - LOMM_DISPLAY_MIN) / (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN); + returnX = qBound(LOMM_DISPLAY_X, LOMM_DISPLAY_X + returnX * LOMM_DISPLAY_WIDTH, LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH); + return returnX; +} + +float LOMMControlDialog::xToDbfs(int x) +{ + float xNorm = static_cast(x - LOMM_DISPLAY_X) / LOMM_DISPLAY_WIDTH; + float dbfs = xNorm * (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN) + LOMM_DISPLAY_MIN; + return dbfs; +} + +void LOMMControlDialog::mousePressEvent(QMouseEvent* event) +{ + if ((event->button() == Qt::LeftButton || event->button() == Qt::MiddleButton) && !(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) + { + const QPoint& p = event->pos(); + + if (LOMM_DISPLAY_X - 10 <= p.x() && p.x() <= LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH + 10) + { + FloatModel* aThresh[] = {&m_controls->m_aThreshHModel, &m_controls->m_aThreshMModel, &m_controls->m_aThreshLModel}; + FloatModel* bThresh[] = {&m_controls->m_bThreshHModel, &m_controls->m_bThreshMModel, &m_controls->m_bThreshLModel}; + + for (int i = 0; i < 3; ++i) + { + if (LOMM_DISPLAY_Y[i * 2] <= p.y() && p.y() <= LOMM_DISPLAY_Y[i * 2 + 1] + LOMM_DISPLAY_HEIGHT) + { + int behavior = (p.x() < dbfsToX(bThresh[i]->value())) ? 0 : (p.x() > dbfsToX(aThresh[i]->value())) ? 1 : 2; + if (event->button() == Qt::MiddleButton) + { + if (behavior == 0 || behavior == 2) {bThresh[i]->reset();} + if (behavior == 1 || behavior == 2) {aThresh[i]->reset();} + return; + } + + m_bandDrag = i; + m_lastMousePos = p; + m_buttonPressed = true; + + m_dragType = behavior; + return; + } + } + } + } +} + +void LOMMControlDialog::mouseMoveEvent(QMouseEvent * event) +{ + if (m_buttonPressed && event->pos() != m_lastMousePos) + { + const float distance = event->pos().x() - m_lastMousePos.x(); + float dbDistance = distance * LOMM_DISPLAY_DB_PER_PIXEL; + m_lastMousePos = event->pos(); + + FloatModel* aModel[] = {&m_controls->m_aThreshHModel, &m_controls->m_aThreshMModel, &m_controls->m_aThreshLModel}; + FloatModel* bModel[] = {&m_controls->m_bThreshHModel, &m_controls->m_bThreshMModel, &m_controls->m_bThreshLModel}; + + float bVal = bModel[m_bandDrag]->value(); + float aVal = aModel[m_bandDrag]->value(); + if (m_dragType == 0) + { + bModel[m_bandDrag]->setValue(bVal + dbDistance); + } + else if (m_dragType == 1) + { + aModel[m_bandDrag]->setValue(aVal + dbDistance); + } + else + { + dbDistance = qBound(bModel[m_bandDrag]->minValue(), bVal + dbDistance, bModel[m_bandDrag]->maxValue()) - bVal; + dbDistance = qBound(aModel[m_bandDrag]->minValue(), aVal + dbDistance, aModel[m_bandDrag]->maxValue()) - aVal; + bModel[m_bandDrag]->setValue(bVal + dbDistance); + aModel[m_bandDrag]->setValue(aVal + dbDistance); + } + } +} + +void LOMMControlDialog::mouseReleaseEvent(QMouseEvent* event) +{ + if (event && event->button() == Qt::LeftButton) + { + m_buttonPressed = false; + } +} + + +} // namespace lmms::gui diff --git a/plugins/LOMM/LOMMControlDialog.h b/plugins/LOMM/LOMMControlDialog.h new file mode 100644 index 00000000000..bf7e67c4ca8 --- /dev/null +++ b/plugins/LOMM/LOMMControlDialog.h @@ -0,0 +1,129 @@ +/* + * LOMMControlDialog.h + * + * Copyright (c) 2023 Lost Robot + * + * 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_GUI_LOMM_CONTROL_DIALOG_H +#define LMMS_GUI_LOMM_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" + +#include +#include + +#include "embed.h" +#include "GuiApplication.h" +#include "Knob.h" +#include "LcdFloatSpinBox.h" +#include "LcdSpinBox.h" +#include "LedCheckBox.h" +#include "MainWindow.h" +#include "PixmapButton.h" + +namespace lmms +{ + +inline constexpr float LOMM_DISPLAY_MIN = -72; +inline constexpr float LOMM_DISPLAY_MAX = 0; +inline constexpr float LOMM_DISPLAY_X = 125; +inline constexpr float LOMM_DISPLAY_Y[6] = {24, 41, 106, 123, 186, 203}; +inline constexpr float LOMM_DISPLAY_WIDTH = 150; +inline constexpr float LOMM_DISPLAY_HEIGHT = 13; +inline constexpr float LOMM_DISPLAY_DB_PER_PIXEL = (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN) / LOMM_DISPLAY_WIDTH; + +class LOMMControls; + + +namespace gui +{ + +class LOMMControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + LOMMControlDialog(LOMMControls* controls); + ~LOMMControlDialog() override = default; + + int dbfsToX(float dbfs); + float xToDbfs(int x); + + Knob* createKnob(KnobType knobType, QWidget* parent, int x, int y, FloatModel* model, const QString& hintText, const QString& unit, const QString& toolTip) + { + Knob* knob = new Knob(knobType, parent); + knob->move(x, y); + knob->setModel(model); + knob->setHintText(hintText, unit); + knob->setToolTip(toolTip); + return knob; + } + + LcdFloatSpinBox* createLcdFloatSpinBox(int integerDigits, int decimalDigits, const QString& color, const QString& unit, QWidget* parent, int x, int y, FloatModel* model, const QString& toolTip) + { + LcdFloatSpinBox* spinBox = new LcdFloatSpinBox(integerDigits, decimalDigits, color, unit, parent); + spinBox->move(x, y); + spinBox->setModel(model); + spinBox->setSeamless(true, true); + spinBox->setToolTip(toolTip); + return spinBox; + } + + PixmapButton* createPixmapButton(const QString& text, QWidget* parent, int x, int y, BoolModel* model, const QString& activeIcon, const QString& inactiveIcon, const QString& tooltip) + { + PixmapButton* button = new PixmapButton(parent, text); + button->move(x, y); + button->setCheckable(true); + if (model) { button->setModel(model); } + button->setActiveGraphic(PLUGIN_NAME::getIconPixmap(activeIcon)); + button->setInactiveGraphic(PLUGIN_NAME::getIconPixmap(inactiveIcon)); + button->setToolTip(tooltip); + return button; + } + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + +private: + LOMMControls* m_controls; + + QPoint m_lastMousePos; + bool m_buttonPressed = false; + int m_bandDrag = 0; + int m_dragType = -1; + + PixmapButton* m_feedbackButton; + PixmapButton* m_lowSideUpwardSuppressButton; + +private slots: + void updateFeedbackVisibility(); + void updateLowSideUpwardSuppressVisibility(); + void updateDisplay(); +}; + + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LOMM_CONTROL_DIALOG_H diff --git a/plugins/LOMM/LOMMControls.cpp b/plugins/LOMM/LOMMControls.cpp new file mode 100644 index 00000000000..d695cf483fd --- /dev/null +++ b/plugins/LOMM/LOMMControls.cpp @@ -0,0 +1,277 @@ +/* + * LOMMControls.cpp + * + * Copyright (c) 2023 Lost Robot + * + * 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 "LOMMControls.h" +#include "LOMM.h" + +#include +#include + +namespace lmms +{ + +LOMMControls::LOMMControls(LOMMEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_depthModel(0.4, 0, 1, 0.00001, this, tr("Depth")), + m_timeModel(1, 0, 10, 0.00001, this, tr("Time")), + m_inVolModel(0, -48, 48, 0.00001, this, tr("Input Volume")), + m_outVolModel(8, -48, 48, 0.00001, this, tr("Output Volume")), + m_upwardModel(1, 0, 2, 0.00001, this, tr("Upward Depth")), + m_downwardModel(1, 0, 2, 0.00001, this, tr("Downward Depth")), + m_split1Model(2500, 20, 20000, 0.01, this, tr("High/Mid Split")), + m_split2Model(88.3, 20, 20000, 0.01, this, tr("Mid/Low Split")), + m_split1EnabledModel(true, this, tr("Enable High/Mid Split")), + m_split2EnabledModel(true, this, tr("Enable Mid/Low Split")), + m_band1EnabledModel(true, this, tr("Enable High Band")), + m_band2EnabledModel(true, this, tr("Enable Mid Band")), + m_band3EnabledModel(true, this, tr("Enable Low Band")), + m_inHighModel(0, -48, 48, 0.00001, this, tr("High Input Volume")), + m_inMidModel(0, -48, 48, 0.00001, this, tr("Mid Input Volume")), + m_inLowModel(0, -48, 48, 0.00001, this, tr("Low Input Volume")), + m_outHighModel(4.6, -48, 48, 0.00001, this, tr("High Output Volume")), + m_outMidModel(0.0, -48, 48, 0.00001, this, tr("Mid Output Volume")), + m_outLowModel(4.6, -48, 48, 0.00001, this, tr("Low Output Volume")), + m_aThreshHModel(-30.3, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold High")), + m_aThreshMModel(-25.0, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold Mid")), + m_aThreshLModel(-28.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold Low")), + m_aRatioHModel(99.99, 1, 99.99, 0.01, this, tr("Above Ratio High")), + m_aRatioMModel(66.7, 1, 99.99, 0.01, this, tr("Above Ratio Mid")), + m_aRatioLModel(66.7, 1, 99.99, 0.01, this, tr("Above Ratio Low")), + m_bThreshHModel(-35.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold High")), + m_bThreshMModel(-36.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold Mid")), + m_bThreshLModel(-35.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold Low")), + m_bRatioHModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio High")), + m_bRatioMModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio Mid")), + m_bRatioLModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio Low")), + m_atkHModel(13.5, 0, 1000, 0.001, this, tr("Attack High")), + m_atkMModel(22.4, 0, 1000, 0.001, this, tr("Attack Mid")), + m_atkLModel(47.8, 0, 1000, 0.001, this, tr("Attack Low")), + m_relHModel(132, 0, 1000, 0.001, this, tr("Release High")), + m_relMModel(282, 0, 1000, 0.001, this, tr("Release Mid")), + m_relLModel(282, 0, 1000, 0.001, this, tr("Release Low")), + m_rmsTimeModel(10, 0, 500, 0.001, this, tr("RMS Time")), + m_kneeModel(6, 0, 36, 0.00001, this, tr("Knee")), + m_rangeModel(36, 0, 96, 0.00001, this, tr("Range")), + m_balanceModel(0, -18, 18, 0.00001, this, tr("Balance")), + m_depthScalingModel(true, this, tr("Scale output volume with Depth")), + m_stereoLinkModel(false, this, tr("Stereo Link")), + m_autoTimeModel(0, 0, 1, 0.00001, this, tr("Auto Time")), + m_mixModel(1, 0, 1, 0.00001, this, tr("Mix")), + m_feedbackModel(false, this, tr("Feedback")), + m_midsideModel(false, this, tr("Mid/Side")), + m_lookaheadEnableModel(false, this, tr("Lookahead")), + m_lookaheadModel(0.f, 0.f, LOMM_MAX_LOOKAHEAD, 0.01, this, tr("Lookahead Length")), + m_lowSideUpwardSuppressModel(false, this, tr("Suppress upward compression for side band")) +{ + auto models = {&m_timeModel, &m_inVolModel, &m_outVolModel, &m_inHighModel, &m_inMidModel, + &m_inLowModel, &m_outHighModel, &m_outMidModel, &m_outLowModel, &m_aRatioHModel, + &m_aRatioMModel, &m_aRatioLModel, &m_bRatioHModel, &m_bRatioMModel, &m_bRatioLModel, + &m_atkHModel, &m_atkMModel, &m_atkLModel, &m_relHModel, &m_relMModel, &m_relLModel, + &m_rmsTimeModel, &m_balanceModel}; + for (auto model : models) { model->setScaleLogarithmic(true); } +} + + +void LOMMControls::resetAllParameters() +{ + int choice = QMessageBox::question(m_view, "Clear Plugin Settings", "Are you sure you want to clear all parameters?\n(This wipes LOMM to a clean slate, not the default preset.)", QMessageBox::Yes | QMessageBox::No); + if (choice != QMessageBox::Yes) { return; } + + // give the user a chance to beg LMMS for forgiveness + addJournalCheckPoint(); + + // This plugin's normal default values are fairly close to what they'd want in most applications. + // The Init button is there so the user can start from a clean slate instead. + // These are those values. + setInitAndReset(m_depthModel, 1); + setInitAndReset(m_timeModel, 1); + setInitAndReset(m_inVolModel, 0); + setInitAndReset(m_outVolModel, 0); + setInitAndReset(m_upwardModel, 1); + setInitAndReset(m_downwardModel, 1); + setInitAndReset(m_split1Model, 2500); + setInitAndReset(m_split2Model, 88); + setInitAndReset(m_split1EnabledModel, true); + setInitAndReset(m_split2EnabledModel, true); + setInitAndReset(m_band1EnabledModel, true); + setInitAndReset(m_band2EnabledModel, true); + setInitAndReset(m_band3EnabledModel, true); + setInitAndReset(m_inHighModel, 0); + setInitAndReset(m_inMidModel, 0); + setInitAndReset(m_inLowModel, 0); + setInitAndReset(m_outHighModel, 0); + setInitAndReset(m_outMidModel, 0); + setInitAndReset(m_outLowModel, 0); + setInitAndReset(m_aThreshHModel, m_aThreshHModel.maxValue()); + setInitAndReset(m_aThreshMModel, m_aThreshMModel.maxValue()); + setInitAndReset(m_aThreshLModel, m_aThreshLModel.maxValue()); + setInitAndReset(m_aRatioHModel, 1); + setInitAndReset(m_aRatioMModel, 1); + setInitAndReset(m_aRatioLModel, 1); + setInitAndReset(m_bThreshHModel, m_bThreshHModel.minValue()); + setInitAndReset(m_bThreshMModel, m_bThreshMModel.minValue()); + setInitAndReset(m_bThreshLModel, m_bThreshLModel.minValue()); + setInitAndReset(m_bRatioHModel, 1); + setInitAndReset(m_bRatioMModel, 1); + setInitAndReset(m_bRatioLModel, 1); + setInitAndReset(m_atkHModel, 13.5); + setInitAndReset(m_atkMModel, 22.4); + setInitAndReset(m_atkLModel, 47.8); + setInitAndReset(m_relHModel, 132); + setInitAndReset(m_relMModel, 282); + setInitAndReset(m_relLModel, 282); + setInitAndReset(m_rmsTimeModel, 10); + setInitAndReset(m_kneeModel, 6); + setInitAndReset(m_rangeModel, 36); + setInitAndReset(m_balanceModel, 0); + setInitAndReset(m_depthScalingModel, true); + setInitAndReset(m_stereoLinkModel, false); + setInitAndReset(m_autoTimeModel, 0); + setInitAndReset(m_mixModel, 1); + setInitAndReset(m_feedbackModel, false); + setInitAndReset(m_midsideModel, false); + setInitAndReset(m_lookaheadEnableModel, false); + setInitAndReset(m_lookaheadModel, 0.f); + setInitAndReset(m_lowSideUpwardSuppressModel, false); +} + + + +void LOMMControls::loadSettings(const QDomElement& parent) +{ + m_depthModel.loadSettings(parent, "depth"); + m_timeModel.loadSettings(parent, "time"); + m_inVolModel.loadSettings(parent, "inVol"); + m_outVolModel.loadSettings(parent, "outVol"); + m_upwardModel.loadSettings(parent, "upward"); + m_downwardModel.loadSettings(parent, "downward"); + m_split1Model.loadSettings(parent, "split1"); + m_split2Model.loadSettings(parent, "split2"); + m_split1EnabledModel.loadSettings(parent, "split1Enabled"); + m_split2EnabledModel.loadSettings(parent, "split2Enabled"); + m_band1EnabledModel.loadSettings(parent, "band1Enabled"); + m_band2EnabledModel.loadSettings(parent, "band2Enabled"); + m_band3EnabledModel.loadSettings(parent, "band3Enabled"); + m_inHighModel.loadSettings(parent, "inHigh"); + m_inMidModel.loadSettings(parent, "inMid"); + m_inLowModel.loadSettings(parent, "inLow"); + m_outHighModel.loadSettings(parent, "outHigh"); + m_outMidModel.loadSettings(parent, "outMid"); + m_outLowModel.loadSettings(parent, "outLow"); + m_aThreshHModel.loadSettings(parent, "aThreshH"); + m_aThreshMModel.loadSettings(parent, "aThreshM"); + m_aThreshLModel.loadSettings(parent, "aThreshL"); + m_aRatioHModel.loadSettings(parent, "aRatioH"); + m_aRatioMModel.loadSettings(parent, "aRatioM"); + m_aRatioLModel.loadSettings(parent, "aRatioL"); + m_bThreshHModel.loadSettings(parent, "bThreshH"); + m_bThreshMModel.loadSettings(parent, "bThreshM"); + m_bThreshLModel.loadSettings(parent, "bThreshL"); + m_bRatioHModel.loadSettings(parent, "bRatioH"); + m_bRatioMModel.loadSettings(parent, "bRatioM"); + m_bRatioLModel.loadSettings(parent, "bRatioL"); + m_atkHModel.loadSettings(parent, "atkH"); + m_atkMModel.loadSettings(parent, "atkM"); + m_atkLModel.loadSettings(parent, "atkL"); + m_relHModel.loadSettings(parent, "relH"); + m_relMModel.loadSettings(parent, "relM"); + m_relLModel.loadSettings(parent, "relL"); + m_rmsTimeModel.loadSettings(parent, "rmsTime"); + m_kneeModel.loadSettings(parent, "knee"); + m_rangeModel.loadSettings(parent, "range"); + m_balanceModel.loadSettings(parent, "balance"); + m_depthScalingModel.loadSettings(parent, "depthScaling"); + m_stereoLinkModel.loadSettings(parent, "stereoLink"); + m_autoTimeModel.loadSettings(parent, "autoTime"); + m_mixModel.loadSettings(parent, "mix"); + m_feedbackModel.loadSettings(parent, "feedback"); + m_midsideModel.loadSettings(parent, "midside"); + m_lookaheadEnableModel.loadSettings(parent, "lookaheadEnable"); + m_lookaheadModel.loadSettings(parent, "lookahead"); + m_lowSideUpwardSuppressModel.loadSettings(parent, "lowSideUpwardSuppress"); +} + + + + +void LOMMControls::saveSettings(QDomDocument& doc, QDomElement& parent) +{ + m_depthModel.saveSettings(doc, parent, "depth"); + m_timeModel.saveSettings(doc, parent, "time"); + m_inVolModel.saveSettings(doc, parent, "inVol"); + m_outVolModel.saveSettings(doc, parent, "outVol"); + m_upwardModel.saveSettings(doc, parent, "upward"); + m_downwardModel.saveSettings(doc, parent, "downward"); + m_split1Model.saveSettings(doc, parent, "split1"); + m_split2Model.saveSettings(doc, parent, "split2"); + m_split1EnabledModel.saveSettings(doc, parent, "split1Enabled"); + m_split2EnabledModel.saveSettings(doc, parent, "split2Enabled"); + m_band1EnabledModel.saveSettings(doc, parent, "band1Enabled"); + m_band2EnabledModel.saveSettings(doc, parent, "band2Enabled"); + m_band3EnabledModel.saveSettings(doc, parent, "band3Enabled"); + m_inHighModel.saveSettings(doc, parent, "inHigh"); + m_inMidModel.saveSettings(doc, parent, "inMid"); + m_inLowModel.saveSettings(doc, parent, "inLow"); + m_outHighModel.saveSettings(doc, parent, "outHigh"); + m_outMidModel.saveSettings(doc, parent, "outMid"); + m_outLowModel.saveSettings(doc, parent, "outLow"); + m_aThreshHModel.saveSettings(doc, parent, "aThreshH"); + m_aThreshMModel.saveSettings(doc, parent, "aThreshM"); + m_aThreshLModel.saveSettings(doc, parent, "aThreshL"); + m_aRatioHModel.saveSettings(doc, parent, "aRatioH"); + m_aRatioMModel.saveSettings(doc, parent, "aRatioM"); + m_aRatioLModel.saveSettings(doc, parent, "aRatioL"); + m_bThreshHModel.saveSettings(doc, parent, "bThreshH"); + m_bThreshMModel.saveSettings(doc, parent, "bThreshM"); + m_bThreshLModel.saveSettings(doc, parent, "bThreshL"); + m_bRatioHModel.saveSettings(doc, parent, "bRatioH"); + m_bRatioMModel.saveSettings(doc, parent, "bRatioM"); + m_bRatioLModel.saveSettings(doc, parent, "bRatioL"); + m_atkHModel.saveSettings(doc, parent, "atkH"); + m_atkMModel.saveSettings(doc, parent, "atkM"); + m_atkLModel.saveSettings(doc, parent, "atkL"); + m_relHModel.saveSettings(doc, parent, "relH"); + m_relMModel.saveSettings(doc, parent, "relM"); + m_relLModel.saveSettings(doc, parent, "relL"); + m_rmsTimeModel.saveSettings(doc, parent, "rmsTime"); + m_kneeModel.saveSettings(doc, parent, "knee"); + m_rangeModel.saveSettings(doc, parent, "range"); + m_balanceModel.saveSettings(doc, parent, "balance"); + m_depthScalingModel.saveSettings(doc, parent, "depthScaling"); + m_stereoLinkModel.saveSettings(doc, parent, "stereoLink"); + m_autoTimeModel.saveSettings(doc, parent, "autoTime"); + m_mixModel.saveSettings(doc, parent, "mix"); + m_feedbackModel.saveSettings(doc, parent, "feedback"); + m_midsideModel.saveSettings(doc, parent, "midside"); + m_lookaheadEnableModel.saveSettings(doc, parent, "lookaheadEnable"); + m_lookaheadModel.saveSettings(doc, parent, "lookahead"); + m_lowSideUpwardSuppressModel.saveSettings(doc, parent, "lowSideUpwardSuppress"); +} + + +} // namespace lmms + + diff --git a/plugins/LOMM/LOMMControls.h b/plugins/LOMM/LOMMControls.h new file mode 100644 index 00000000000..3e53254266b --- /dev/null +++ b/plugins/LOMM/LOMMControls.h @@ -0,0 +1,136 @@ +/* + * LOMMControls.h + * + * Copyright (c) 2023 Lost Robot + * + * 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_LOMM_CONTROLS_H +#define LMMS_LOMM_CONTROLS_H + +#include "LOMMControlDialog.h" +#include "EffectControls.h" + +namespace lmms +{ +class LOMMEffect; + +namespace gui +{ +class LOMMControlDialog; +} + +class LOMMControls : public EffectControls +{ + Q_OBJECT +public: + LOMMControls(LOMMEffect* effect); + ~LOMMControls() override = default; + + void saveSettings(QDomDocument & doc, QDomElement & parent) override; + void loadSettings(const QDomElement & parent) override; + inline QString nodeName() const override + { + return "LOMMControls"; + } + + int controlCount() override + { + return 49; + } + + gui::EffectControlDialog* createView() override + { + m_view = new gui::LOMMControlDialog(this); + return m_view; + } + + template + void setInitAndReset(AutomatableModel& model, T initValue) + { + model.setInitValue(initValue); + model.reset(); + } + +public slots: + void resetAllParameters(); + +private: + LOMMEffect* m_effect; + gui::LOMMControlDialog* m_view; + + FloatModel m_depthModel; + FloatModel m_timeModel; + FloatModel m_inVolModel; + FloatModel m_outVolModel; + FloatModel m_upwardModel; + FloatModel m_downwardModel; + FloatModel m_split1Model; + FloatModel m_split2Model; + BoolModel m_split1EnabledModel; + BoolModel m_split2EnabledModel; + BoolModel m_band1EnabledModel; + BoolModel m_band2EnabledModel; + BoolModel m_band3EnabledModel; + FloatModel m_inHighModel; + FloatModel m_inMidModel; + FloatModel m_inLowModel; + FloatModel m_outHighModel; + FloatModel m_outMidModel; + FloatModel m_outLowModel; + FloatModel m_aThreshHModel; + FloatModel m_aThreshMModel; + FloatModel m_aThreshLModel; + FloatModel m_aRatioHModel; + FloatModel m_aRatioMModel; + FloatModel m_aRatioLModel; + FloatModel m_bThreshHModel; + FloatModel m_bThreshMModel; + FloatModel m_bThreshLModel; + FloatModel m_bRatioHModel; + FloatModel m_bRatioMModel; + FloatModel m_bRatioLModel; + FloatModel m_atkHModel; + FloatModel m_atkMModel; + FloatModel m_atkLModel; + FloatModel m_relHModel; + FloatModel m_relMModel; + FloatModel m_relLModel; + FloatModel m_rmsTimeModel; + FloatModel m_kneeModel; + FloatModel m_rangeModel; + FloatModel m_balanceModel; + BoolModel m_depthScalingModel; + BoolModel m_stereoLinkModel; + FloatModel m_autoTimeModel; + FloatModel m_mixModel; + BoolModel m_feedbackModel; + BoolModel m_midsideModel; + BoolModel m_lookaheadEnableModel; + FloatModel m_lookaheadModel; + BoolModel m_lowSideUpwardSuppressModel; + + friend class gui::LOMMControlDialog; + friend class LOMMEffect; +}; + +} // namespace lmms + +#endif // LMMS_LOMM_CONTROLS_H diff --git a/plugins/LOMM/artwork.png b/plugins/LOMM/artwork.png new file mode 100644 index 00000000000..cfc65908ff1 Binary files /dev/null and b/plugins/LOMM/artwork.png differ diff --git a/plugins/LOMM/crossover_led_green.png b/plugins/LOMM/crossover_led_green.png new file mode 100644 index 00000000000..440eb82dd1f Binary files /dev/null and b/plugins/LOMM/crossover_led_green.png differ diff --git a/plugins/LOMM/crossover_led_off.png b/plugins/LOMM/crossover_led_off.png new file mode 100644 index 00000000000..2fd7f721ca2 Binary files /dev/null and b/plugins/LOMM/crossover_led_off.png differ diff --git a/plugins/LOMM/depthScaling_active.png b/plugins/LOMM/depthScaling_active.png new file mode 100644 index 00000000000..ff53a77e01d Binary files /dev/null and b/plugins/LOMM/depthScaling_active.png differ diff --git a/plugins/LOMM/depthScaling_inactive.png b/plugins/LOMM/depthScaling_inactive.png new file mode 100644 index 00000000000..ba806b6c35f Binary files /dev/null and b/plugins/LOMM/depthScaling_inactive.png differ diff --git a/plugins/LOMM/feedback_active.png b/plugins/LOMM/feedback_active.png new file mode 100644 index 00000000000..426abb50683 Binary files /dev/null and b/plugins/LOMM/feedback_active.png differ diff --git a/plugins/LOMM/feedback_inactive.png b/plugins/LOMM/feedback_inactive.png new file mode 100644 index 00000000000..b3e7d635adc Binary files /dev/null and b/plugins/LOMM/feedback_inactive.png differ diff --git a/plugins/LOMM/high_band_active.png b/plugins/LOMM/high_band_active.png new file mode 100644 index 00000000000..e3c225e2d34 Binary files /dev/null and b/plugins/LOMM/high_band_active.png differ diff --git a/plugins/LOMM/high_band_inactive.png b/plugins/LOMM/high_band_inactive.png new file mode 100644 index 00000000000..43a24cc8f7b Binary files /dev/null and b/plugins/LOMM/high_band_inactive.png differ diff --git a/plugins/LOMM/init_active.png b/plugins/LOMM/init_active.png new file mode 100644 index 00000000000..3401a74c192 Binary files /dev/null and b/plugins/LOMM/init_active.png differ diff --git a/plugins/LOMM/init_inactive.png b/plugins/LOMM/init_inactive.png new file mode 100644 index 00000000000..dfd847c3251 Binary files /dev/null and b/plugins/LOMM/init_inactive.png differ diff --git a/plugins/LOMM/logo.png b/plugins/LOMM/logo.png new file mode 100644 index 00000000000..9340da708dd Binary files /dev/null and b/plugins/LOMM/logo.png differ diff --git a/plugins/LOMM/lookahead_active.png b/plugins/LOMM/lookahead_active.png new file mode 100644 index 00000000000..78fc1ba03a1 Binary files /dev/null and b/plugins/LOMM/lookahead_active.png differ diff --git a/plugins/LOMM/lookahead_inactive.png b/plugins/LOMM/lookahead_inactive.png new file mode 100644 index 00000000000..8e4e2ddb338 Binary files /dev/null and b/plugins/LOMM/lookahead_inactive.png differ diff --git a/plugins/LOMM/lowSideUpwardSuppress_active.png b/plugins/LOMM/lowSideUpwardSuppress_active.png new file mode 100644 index 00000000000..39e6d746cc5 Binary files /dev/null and b/plugins/LOMM/lowSideUpwardSuppress_active.png differ diff --git a/plugins/LOMM/lowSideUpwardSuppress_inactive.png b/plugins/LOMM/lowSideUpwardSuppress_inactive.png new file mode 100644 index 00000000000..8e61e129d5e Binary files /dev/null and b/plugins/LOMM/lowSideUpwardSuppress_inactive.png differ diff --git a/plugins/LOMM/low_band_active.png b/plugins/LOMM/low_band_active.png new file mode 100644 index 00000000000..fc3ca34c11f Binary files /dev/null and b/plugins/LOMM/low_band_active.png differ diff --git a/plugins/LOMM/low_band_inactive.png b/plugins/LOMM/low_band_inactive.png new file mode 100644 index 00000000000..48041495e2c Binary files /dev/null and b/plugins/LOMM/low_band_inactive.png differ diff --git a/plugins/LOMM/mid_band_active.png b/plugins/LOMM/mid_band_active.png new file mode 100644 index 00000000000..5a714a01972 Binary files /dev/null and b/plugins/LOMM/mid_band_active.png differ diff --git a/plugins/LOMM/mid_band_inactive.png b/plugins/LOMM/mid_band_inactive.png new file mode 100644 index 00000000000..6ccabc0ad9f Binary files /dev/null and b/plugins/LOMM/mid_band_inactive.png differ diff --git a/plugins/LOMM/midside_active.png b/plugins/LOMM/midside_active.png new file mode 100644 index 00000000000..9caf8b69147 Binary files /dev/null and b/plugins/LOMM/midside_active.png differ diff --git a/plugins/LOMM/midside_inactive.png b/plugins/LOMM/midside_inactive.png new file mode 100644 index 00000000000..7b14b75df01 Binary files /dev/null and b/plugins/LOMM/midside_inactive.png differ diff --git a/plugins/LOMM/split1Enabled_active.png b/plugins/LOMM/split1Enabled_active.png new file mode 100644 index 00000000000..6b3f8ec62d8 Binary files /dev/null and b/plugins/LOMM/split1Enabled_active.png differ diff --git a/plugins/LOMM/split1Enabled_inactive.png b/plugins/LOMM/split1Enabled_inactive.png new file mode 100644 index 00000000000..7577806a0bf Binary files /dev/null and b/plugins/LOMM/split1Enabled_inactive.png differ diff --git a/plugins/LOMM/split2Enabled_active.png b/plugins/LOMM/split2Enabled_active.png new file mode 100644 index 00000000000..b7b162ceb8f Binary files /dev/null and b/plugins/LOMM/split2Enabled_active.png differ diff --git a/plugins/LOMM/split2Enabled_inactive.png b/plugins/LOMM/split2Enabled_inactive.png new file mode 100644 index 00000000000..678d0565bfa Binary files /dev/null and b/plugins/LOMM/split2Enabled_inactive.png differ diff --git a/plugins/LOMM/stereoLink_active.png b/plugins/LOMM/stereoLink_active.png new file mode 100644 index 00000000000..2ba9c4b8857 Binary files /dev/null and b/plugins/LOMM/stereoLink_active.png differ diff --git a/plugins/LOMM/stereoLink_inactive.png b/plugins/LOMM/stereoLink_inactive.png new file mode 100644 index 00000000000..9e5d5b88402 Binary files /dev/null and b/plugins/LOMM/stereoLink_inactive.png differ diff --git a/plugins/LadspaEffect/CMakeLists.txt b/plugins/LadspaEffect/CMakeLists.txt index 951615ad4d0..202a8dd0473 100644 --- a/plugins/LadspaEffect/CMakeLists.txt +++ b/plugins/LadspaEffect/CMakeLists.txt @@ -1,25 +1,25 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(ladspaeffect LadspaEffect.cpp LadspaControls.cpp LadspaControlDialog.cpp LadspaSubPluginFeatures.cpp LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaSubPluginFeatures.h MOCFILES LadspaEffect.h LadspaControls.h LadspaControlDialog.h EMBEDDED_RESOURCES logo.png) +BUILD_PLUGIN(ladspaeffect LadspaEffect.cpp LadspaControls.cpp LadspaControlDialog.cpp LadspaMatrixControlDialog.cpp LadspaSubPluginFeatures.cpp LadspaWidgetFactory.cpp LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaMatrixControlDialog.h LadspaSubPluginFeatures.h LadspaWidgetFactory.h MOCFILES LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaMatrixControlDialog.h EMBEDDED_RESOURCES logo.png) SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/ladspa") -IF(WANT_CAPS) +IF(LMMS_HAVE_CAPS) ADD_SUBDIRECTORY(caps) -ENDIF(WANT_CAPS) +ENDIF() -IF(WANT_TAP) +IF(LMMS_HAVE_TAP) ADD_SUBDIRECTORY(tap) -ENDIF(WANT_TAP) +ENDIF() -IF(WANT_SWH) +IF(LMMS_HAVE_SWH) ADD_SUBDIRECTORY(swh) -ENDIF(WANT_SWH) +ENDIF() -IF(WANT_CMT) +IF(LMMS_HAVE_CMT) ADD_SUBDIRECTORY(cmt) -ENDIF(WANT_CMT) +ENDIF() -IF(WANT_CALF) +IF(LMMS_HAVE_CALF) ADD_SUBDIRECTORY(calf) -ENDIF(WANT_CALF) +ENDIF() diff --git a/plugins/LadspaEffect/LadspaControls.h b/plugins/LadspaEffect/LadspaControls.h index 2bef0c85629..c91f3badd10 100644 --- a/plugins/LadspaEffect/LadspaControls.h +++ b/plugins/LadspaEffect/LadspaControls.h @@ -27,6 +27,7 @@ #include "EffectControls.h" #include "LadspaControlDialog.h" +#include "LadspaMatrixControlDialog.h" namespace lmms { @@ -59,7 +60,7 @@ class LadspaControls : public EffectControls gui::EffectControlDialog* createView() override { - return new gui::LadspaControlDialog( this ); + return new gui::LadspaMatrixControlDialog( this ); } @@ -79,6 +80,7 @@ protected slots: friend class gui::LadspaControlDialog; + friend class gui::LadspaMatrixControlDialog; friend class LadspaEffect; diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp new file mode 100644 index 00000000000..88810cee639 --- /dev/null +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp @@ -0,0 +1,243 @@ +/* + * LadspaMatrixControlDialog.h - Dialog for displaying and editing control port + * values for LADSPA plugins in a matrix display + * + * Copyright (c) 2015 Michael Gregorius + * + * This file is part of LMMS - http://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 +#include +#include +#include +#include +#include +#include + + +#include "LadspaBase.h" +#include "LadspaControl.h" +#include "LadspaEffect.h" +#include "LadspaMatrixControlDialog.h" +#include "LadspaWidgetFactory.h" +#include "LadspaControlView.h" +#include "LedCheckBox.h" + +#include "GuiApplication.h" +#include "MainWindow.h" + + +namespace lmms::gui +{ + +LadspaMatrixControlDialog::LadspaMatrixControlDialog(LadspaControls * ladspaControls) : + EffectControlDialog(ladspaControls), + m_scrollArea(nullptr), + m_stereoLink(nullptr) +{ + QVBoxLayout * mainLayout = new QVBoxLayout(this); + + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(true); + m_scrollArea->setFrameShape(QFrame::NoFrame); + // Set to always on so that the elements do not move around when the + // scroll bar is hidden or shown. + m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + + // Add a scroll area that grows + mainLayout->addWidget(m_scrollArea, 1); + + // Populate the parameter matrix and put it into the scroll area + updateEffectView(ladspaControls); + + // Add button to link all channels if there's more than one channel + if (getChannelCount() > 1) + { + mainLayout->addSpacing(3); + + m_stereoLink = new LedCheckBox(tr("Link Channels"), this, QString(), LedCheckBox::LedColor::Green, false); + m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); + mainLayout->addWidget(m_stereoLink, 0, Qt::AlignCenter); + } +} + +bool LadspaMatrixControlDialog::isResizable() const +{ + return true; +} + +bool LadspaMatrixControlDialog::needsLinkColumn() const +{ + LadspaControls * ladspaControls = getLadspaControls(); + + ch_cnt_t const channelCount = getChannelCount(); + for (ch_cnt_t i = 0; i < channelCount; ++i) + { + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + if (ladspaControl->m_link) + { + return true; + } + } + } + + return false; +} + +void LadspaMatrixControlDialog::arrangeControls(QWidget * parent, QGridLayout* gridLayout) +{ + LadspaControls * ladspaControls = getLadspaControls(); + + int const headerRow = 0; + int const linkColumn = 0; + + bool const linkColumnNeeded = needsLinkColumn(); + if (linkColumnNeeded) + { + gridLayout->addWidget(new QLabel("" + tr("Link") + "", parent), headerRow, linkColumn, Qt::AlignHCenter); + + // If there's a link column then it should not stretch + gridLayout->setColumnStretch(linkColumn, 0); + } + + int const channelStartColumn = linkColumnNeeded ? 1 : 0; + + // The header row should not grow vertically + gridLayout->setRowStretch(0, 0); + + // Records the maximum row with parameters so that we can add a vertical spacer after that row + int maxRow = 0; + + // Iterate the channels and add widgets for each control + // Note: the code assumes that all channels have the same structure, i.e. that all channels + // have the same number of parameters which are in the same order. + ch_cnt_t const numberOfChannels = getChannelCount(); + for (ch_cnt_t i = 0; i < numberOfChannels; ++i) + { + int currentChannelColumn = channelStartColumn + i; + gridLayout->setColumnStretch(currentChannelColumn, 1); + + // First add the channel header with the channel number + gridLayout->addWidget(new QLabel("" + tr("Channel %1").arg(QString::number(i + 1)) + "", parent), headerRow, currentChannelColumn, Qt::AlignHCenter); + + int currentRow = 1; + + if (i == 0) + { + // Configure the current parameter row to not stretch. + // Only do this once, i.e. when working with the first channel. + gridLayout->setRowStretch(currentRow, 0); + } + + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + // Only use the first channel to determine if we need to add link controls + if (i == 0 && ladspaControl->m_link) + { + LedCheckBox * linkCheckBox = new LedCheckBox("", parent, "", LedCheckBox::LedColor::Green); + linkCheckBox->setModel(&ladspaControl->m_linkEnabledModel); + linkCheckBox->setToolTip(tr("Link channels")); + gridLayout->addWidget(linkCheckBox, currentRow, linkColumn, Qt::AlignHCenter); + } + + QWidget * controlWidget = LadspaWidgetFactory::createWidget(ladspaControl, this); + if (controlWidget) + { + gridLayout->addWidget(controlWidget, currentRow, currentChannelColumn); + } + + // Record the maximum row so that we add a vertical spacer after that row + maxRow = std::max(maxRow, currentRow); + + ++currentRow; + } + } + + // Add a spacer item after the maximum row + QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + gridLayout->addItem(spacer, maxRow + 1, 0); +} + +QWidget * LadspaMatrixControlDialog::createMatrixWidget() +{ + QWidget *widget = new QWidget(this); + QGridLayout *gridLayout = new QGridLayout(widget); + gridLayout->setMargin(0); + widget->setLayout(gridLayout); + + arrangeControls(widget, gridLayout); + + return widget; +} + +void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls) +{ + m_effectControls = ladspaControls; + + // No need to delete the existing widget as it's deleted + // by the scroll view when we replace it. + QWidget * matrixWidget = createMatrixWidget(); + m_scrollArea->setWidget(matrixWidget); + + // Make sure that the horizontal scroll bar does not show + // From: https://forum.qt.io/topic/13374/solved-qscrollarea-vertical-scroll-only/4 + m_scrollArea->setMinimumWidth(matrixWidget->minimumSizeHint().width() + m_scrollArea->verticalScrollBar()->width()); + + + // Make sure that the widget is shown without a scrollbar whenever possible + // If the widget fits on the workspace we use the height of the widget as the minimum size of the scroll area. + // This will ensure that the scrollbar is not shown initially (and never will be). + // If the widget is larger than the workspace then we want it to mostly cover the workspace. + // + // This is somewhat ugly but I have no idea how to control the initial size of the scroll area otherwise + auto const workspaceSize = getGUI()->mainWindow()->workspace()->viewport()->size(); + // Make sure that we always account a minumum height for the workspace, i.e. that we never compute + // something close to 0 if the LMMS window is very small + int workspaceHeight = qMax(200, static_cast(workspaceSize.height() * 0.9)); + int minOfWidgetAndWorkspace = qMin(matrixWidget->minimumSizeHint().height(), workspaceHeight); + m_scrollArea->setMinimumHeight(minOfWidgetAndWorkspace); + + if (getChannelCount() > 1 && m_stereoLink != nullptr) + { + m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); + } + + connect(ladspaControls, &LadspaControls::effectModelChanged, + this, &LadspaMatrixControlDialog::updateEffectView, + Qt::DirectConnection); +} + +LadspaControls * LadspaMatrixControlDialog::getLadspaControls() const +{ + return dynamic_cast(m_effectControls); +} + +ch_cnt_t LadspaMatrixControlDialog::getChannelCount() const +{ + return getLadspaControls()->m_processors; +} + +} // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.h b/plugins/LadspaEffect/LadspaMatrixControlDialog.h new file mode 100644 index 00000000000..c5949fa15d2 --- /dev/null +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.h @@ -0,0 +1,91 @@ +/* + * LadspaMatrixControlDialog.h - Dialog for displaying and editing control port + * values for LADSPA plugins in a matrix display + * + * Copyright (c) 2015 Michael Gregorius + * + * This file is part of LMMS - http://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 LADSPA_MATRIX_CONTROL_DIALOG_H +#define LADSPA_MATRIX_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" + +#include "lmms_basics.h" + + +class QGridLayout; +class QScrollArea; + +namespace lmms +{ + +class LadspaControls; + +namespace gui +{ + +class LedCheckBox; + + +class LadspaMatrixControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + LadspaMatrixControlDialog(LadspaControls* ctl); + bool isResizable() const override; + +private slots: + void updateEffectView(LadspaControls* ctl); + +private: + /** + * @brief Checks if a link column is needed for the current effect controls. + * @return true if a link column is needed. + */ + bool needsLinkColumn() const; + + /** + * @brief Arranges widgets for the current controls in a grid/matrix layout. + * @param parent The parent of all created widgets + * @param gridLayout The layout into which the controls are organized + */ + void arrangeControls(QWidget * parent, QGridLayout* gridLayout); + + /** + * @brief Creates a widget that holds the widgets of the current controls in a matrix arrangement. + * @param ladspaControls + * @return + */ + QWidget * createMatrixWidget(); + + LadspaControls * getLadspaControls() const; + ch_cnt_t getChannelCount() const; + +private: + QScrollArea* m_scrollArea; + LedCheckBox* m_stereoLink; +}; + +} // namespace gui + +} // namespace lmms + +#endif diff --git a/plugins/LadspaEffect/LadspaWidgetFactory.cpp b/plugins/LadspaEffect/LadspaWidgetFactory.cpp new file mode 100644 index 00000000000..0491fd66187 --- /dev/null +++ b/plugins/LadspaEffect/LadspaWidgetFactory.cpp @@ -0,0 +1,83 @@ +/* + * LadspaWidgetFactory.cpp - Factory that creates widgets for LADSPA ports + * + * Copyright (c) 2006-2008 Danny McRae + * Copyright (c) 2009 Tobias Doerffel + * Copyright (c) 2015-2023 Michael Gregorius + * + * 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 "LadspaWidgetFactory.h" + +#include "LadspaControl.h" +#include "LadspaBase.h" + +#include "BarModelEditor.h" +#include "LedCheckBox.h" +#include "TempoSyncBarModelEditor.h" + +#include +#include + + +namespace lmms::gui +{ + +QWidget * LadspaWidgetFactory::createWidget(LadspaControl * ladspaControl, QWidget * parent) +{ + auto const * port = ladspaControl->port(); + + QString const name = port->name; + + switch (port->data_type) + { + case BufferDataType::Toggled: + { + // The actual check box is put into a widget because LedCheckBox does not play nice with layouts. + // Putting it directly into a grid layout disables the resizing behavior of all columns where it + // appears. Hence we put it into the layout of a widget that knows how to play nice with layouts. + QWidget * widgetWithLayout = new QWidget(parent); + QHBoxLayout * layout = new QHBoxLayout(widgetWithLayout); + layout->setContentsMargins(0, 0, 0, 0); + LedCheckBox * toggle = new LedCheckBox( + name, parent, QString(), LedCheckBox::LedColor::Green, false); + toggle->setModel(ladspaControl->toggledModel()); + layout->addWidget(toggle, 0, Qt::AlignLeft); + + return widgetWithLayout; + } + + case BufferDataType::Integer: + case BufferDataType::Enum: + case BufferDataType::Floating: + return new BarModelEditor(name, ladspaControl->knobModel(), parent); + + case BufferDataType::Time: + return new TempoSyncBarModelEditor(name, ladspaControl->tempoSyncKnobModel(), parent); + + default: + return new QLabel(QObject::tr("%1 (unsupported)").arg(name), parent); + } + + return nullptr; +} + +} // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaWidgetFactory.h b/plugins/LadspaEffect/LadspaWidgetFactory.h new file mode 100644 index 00000000000..807334d327f --- /dev/null +++ b/plugins/LadspaEffect/LadspaWidgetFactory.h @@ -0,0 +1,49 @@ +/* + * LadspaWidgetFactory.h - Factory that creates widgets for LADSPA ports + * + * Copyright (c) 2023 Michael Gregorius + * + * 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_GUI_LADSPA_WIDGET_FACTORY_H +#define LMMS_GUI_LADSPA_WIDGET_FACTORY_H + + +class QWidget; + +namespace lmms +{ + +class LadspaControl; + +namespace gui +{ + +class LadspaWidgetFactory +{ +public: + static QWidget * createWidget(LadspaControl * ladspaControl, QWidget * parent); +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LADSPA_WIDGET_FACTORY_H diff --git a/plugins/LadspaEffect/swh/ladspa b/plugins/LadspaEffect/swh/ladspa index d99a0db521d..02bda232041 160000 --- a/plugins/LadspaEffect/swh/ladspa +++ b/plugins/LadspaEffect/swh/ladspa @@ -1 +1 @@ -Subproject commit d99a0db521d13a87bdaa418c674ca8858e484452 +Subproject commit 02bda232041380c2846414945798cbbfecb2f3f2 diff --git a/plugins/LadspaEffect/tap/tap-plugins b/plugins/LadspaEffect/tap/tap-plugins index 198b84e6ab3..85640223047 160000 --- a/plugins/LadspaEffect/tap/tap-plugins +++ b/plugins/LadspaEffect/tap/tap-plugins @@ -1 +1 @@ -Subproject commit 198b84e6ab37a9c979435cdb8f0a27a0e9a2934f +Subproject commit 85640223047d49a305e90ba1b92303eb066ba474 diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.cpp b/plugins/Lv2Effect/Lv2FxControlDialog.cpp index 5265cb1813d..73890937c04 100644 --- a/plugins/Lv2Effect/Lv2FxControlDialog.cpp +++ b/plugins/Lv2Effect/Lv2FxControlDialog.cpp @@ -72,4 +72,13 @@ void Lv2FxControlDialog::modelChanged() } + + +void Lv2FxControlDialog::hideEvent(QHideEvent *event) +{ + closeHelpWindow(); + QWidget::hideEvent(event); +} + + } // namespace lmms::gui diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.h b/plugins/Lv2Effect/Lv2FxControlDialog.h index 45c14c2c0f1..f38c0364bfa 100644 --- a/plugins/Lv2Effect/Lv2FxControlDialog.h +++ b/plugins/Lv2Effect/Lv2FxControlDialog.h @@ -46,6 +46,7 @@ class Lv2FxControlDialog : public EffectControlDialog, public Lv2ViewBase private: Lv2FxControls *lv2Controls(); void modelChanged() final; + void hideEvent(QHideEvent *event) override; }; diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 32f81d23c25..841b8a89ad2 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -295,6 +295,15 @@ void Lv2InsView::dropEvent(QDropEvent *_de) +void Lv2InsView::hideEvent(QHideEvent *event) +{ + closeHelpWindow(); + QWidget::hideEvent(event); +} + + + + void Lv2InsView::modelChanged() { Lv2ViewBase::modelChanged(castModel()); diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h index 2cd73632da2..5e255e0dfbe 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.h +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -124,6 +124,7 @@ Q_OBJECT protected: void dragEnterEvent(QDragEnterEvent *_dee) override; void dropEvent(QDropEvent *_de) override; + void hideEvent(QHideEvent* event) override; private: void modelChanged() override; diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index df968e36ab3..2600a40f2f9 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -27,6 +27,7 @@ #include "MidiExport.h" +#include "Engine.h" #include "TrackContainer.h" #include "DataFile.h" #include "InstrumentTrack.h" @@ -279,6 +280,7 @@ void MidiExport::writeMidiClip(MidiNoteVector &midiClip, const QDomNode& n, mnote.volume = qMin(qRound(base_volume * LocaleHelper::toDouble(note.attribute("vol", "100")) * (127.0 / 200.0)), 127); mnote.time = base_time + note.attribute("pos", "0").toInt(); mnote.duration = note.attribute("len", "0").toInt(); + mnote.type = static_cast(note.attribute("type", "0").toInt()); midiClip.push_back(mnote); } } @@ -311,6 +313,7 @@ void MidiExport::writePatternClip(MidiNoteVector& src, MidiNoteVector& dst, note.pitch = srcNote.pitch; note.time = base + time; note.volume = srcNote.volume; + note.type = srcNote.type; dst.push_back(note); } } @@ -329,9 +332,9 @@ void MidiExport::processPatternNotes(MidiNoteVector& nv, int cutPos) next = cur; cur = it->time; } - if (it->duration < 0) + if (it->type == Note::Type::Step) { - it->duration = qMin(qMin(-it->duration, next - cur), cutPos - it->time); + it->duration = qMin(qMin(DefaultBeatLength, next - cur), cutPos - it->time); } } } diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 1e355e45ac9..7c77c7af252 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -30,6 +30,7 @@ #include "ExportFilter.h" #include "MidiFile.hpp" +#include "Note.h" class QDomNode; @@ -46,6 +47,7 @@ struct MidiNote uint8_t pitch; int duration; uint8_t volume; + Note::Type type; inline bool operator<(const MidiNote &b) const { @@ -63,6 +65,16 @@ class MidiExport: public ExportFilter MidiExport(); ~MidiExport() override = default; + // Default Beat Length in ticks for step notes + // TODO: The beat length actually varies per note, however the method that + // calculates it (InstrumentTrack::beatLen) requires a NotePlayHandle to do + // so. While we don't figure out a way to hold the beat length of each note + // on its member variables, we will use a default value as a beat length that + // will be used as an upper limit of the midi note length. This doesn't worsen + // the current logic used for MidiExport because right now the beat length is + // not even considered during the generation of the MIDI. + static constexpr int DefaultBeatLength = 1500; + gui::PluginView* instantiateView(QWidget *) override { return nullptr; diff --git a/plugins/Nes/Nes.cpp b/plugins/Nes/Nes.cpp index 47122a0c602..2c0907a197e 100644 --- a/plugins/Nes/Nes.cpp +++ b/plugins/Nes/Nes.cpp @@ -719,7 +719,6 @@ namespace gui { -QPixmap * NesInstrumentView::s_artwork = nullptr; NesInstrumentView::NesInstrumentView( Instrument * instrument, QWidget * parent ) : @@ -728,12 +727,8 @@ NesInstrumentView::NesInstrumentView( Instrument * instrument, QWidget * parent setAutoFillBackground( true ); QPalette pal; - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( "artwork" ) ); - } - - pal.setBrush( backgroundRole(), *s_artwork ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + pal.setBrush(backgroundRole(), s_artwork); setPalette( pal ); const int KNOB_Y1 = 24; diff --git a/plugins/Nes/Nes.h b/plugins/Nes/Nes.h index 3ddf0fc9a0d..b4102f31d10 100644 --- a/plugins/Nes/Nes.h +++ b/plugins/Nes/Nes.h @@ -372,7 +372,6 @@ class NesInstrumentView : public InstrumentViewFixedSize Knob * m_masterVolKnob; Knob * m_vibratoKnob; - static QPixmap * s_artwork; }; diff --git a/plugins/Organic/Organic.cpp b/plugins/Organic/Organic.cpp index a70da642156..761010922a3 100644 --- a/plugins/Organic/Organic.cpp +++ b/plugins/Organic/Organic.cpp @@ -60,7 +60,6 @@ Plugin::Descriptor PLUGIN_EXPORT organic_plugin_descriptor = } -QPixmap * gui::OrganicInstrumentView::s_artwork = nullptr; float * OrganicInstrument::s_harmonics = nullptr; /*********************************************************************** @@ -420,8 +419,8 @@ OrganicInstrumentView::OrganicInstrumentView( Instrument * _instrument, setAutoFillBackground( true ); QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( - "artwork" ) ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + pal.setBrush(backgroundRole(), s_artwork); setPalette( pal ); // setup knob for FX1 @@ -451,12 +450,6 @@ OrganicInstrumentView::OrganicInstrumentView( Instrument * _instrument, oi, SLOT( randomiseSettings() ) ); - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( - "artwork" ) ); - } - } diff --git a/plugins/Organic/Organic.h b/plugins/Organic/Organic.h index 6c53e84ec06..a46b7882ffa 100644 --- a/plugins/Organic/Organic.h +++ b/plugins/Organic/Organic.h @@ -227,7 +227,6 @@ class OrganicInstrumentView : public InstrumentViewFixedSize int m_numOscillators; - static QPixmap * s_artwork; protected slots: void updateKnobHint(); diff --git a/plugins/Sf2Player/CMakeLists.txt b/plugins/Sf2Player/CMakeLists.txt index 4679a94bda3..1d004a6c595 100644 --- a/plugins/Sf2Player/CMakeLists.txt +++ b/plugins/Sf2Player/CMakeLists.txt @@ -1,13 +1,10 @@ if(LMMS_HAVE_FLUIDSYNTH) include(BuildPlugin) - include_directories(${SAMPLERATE_INCLUDE_DIRS}) - link_directories(${SAMPLERATE_LIBRARY_DIRS}) - link_libraries(${SAMPLERATE_LIBRARIES}) build_plugin(sf2player Sf2Player.cpp Sf2Player.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES Sf2Player.h PatchesDialog.h UICFILES PatchesDialog.ui EMBEDDED_RESOURCES *.png ) - target_link_libraries(sf2player fluidsynth) + target_link_libraries(sf2player fluidsynth SampleRate::samplerate) endif() diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 79bd4b97686..7795671c55e 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -73,8 +73,8 @@ 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. + * A non-owning reference to a single FluidSynth voice. Captures some initial + * properties of the referenced voice to help manage changes to it over time. */ class FluidVoice { @@ -82,12 +82,16 @@ class FluidVoice //! 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)} + m_id{fluid_voice_get_id(voice)}, + m_coarseTune{fluid_voice_gen_get(voice, GEN_COARSETUNE)} { } //! Get a pointer to the referenced voice. fluid_voice_t* get() const noexcept { return m_voice; } + //! Get the original coarse tuning of the referenced voice. + float coarseTune() const noexcept { return m_coarseTune; } + //! Test whether this object still refers to the original voice. bool isValid() const { @@ -97,6 +101,7 @@ class FluidVoice private: fluid_voice_t* m_voice; unsigned int m_id; + float m_coarseTune; }; struct Sf2PluginData @@ -116,12 +121,6 @@ struct Sf2PluginData -// Static map of current sfonts -QMap Sf2Instrument::s_fonts; -QMutex Sf2Instrument::s_fontsMutex; - - - Sf2Instrument::Sf2Instrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &sf2player_plugin_descriptor ), m_srcState( nullptr ), @@ -370,31 +369,12 @@ void Sf2Instrument::freeFont() { m_synthMutex.lock(); - if ( m_font != nullptr ) + if (m_font != nullptr) { - s_fontsMutex.lock(); - --(m_font->refCount); - - // No more references - if( m_font->refCount <= 0 ) - { - qDebug() << "Really deleting " << m_filename; - - fluid_synth_sfunload( m_synth, m_fontId, true ); - s_fonts.remove( m_filename ); - delete m_font; - } - // Just remove our reference - else - { - qDebug() << "un-referencing " << m_filename; - - fluid_synth_remove_sfont( m_synth, m_font->fluidFont ); - } - s_fontsMutex.unlock(); - + fluid_synth_sfunload(m_synth, m_fontId, true); m_font = nullptr; } + m_synthMutex.unlock(); } @@ -408,49 +388,29 @@ void Sf2Instrument::openFile( const QString & _sf2File, bool updateTrackName ) char * sf2Ascii = qstrdup( qPrintable( PathUtil::toAbsolute( _sf2File ) ) ); QString relativePath = PathUtil::toShortestRelative( _sf2File ); - // free reference to soundfont if one is selected + // free the soundfont if one is selected freeFont(); m_synthMutex.lock(); - s_fontsMutex.lock(); - // Increment Reference - if( s_fonts.contains( relativePath ) ) + bool loaded = false; + if (fluid_is_soundfont(sf2Ascii)) { - qDebug() << "Using existing reference to " << relativePath; + m_fontId = fluid_synth_sfload(m_synth, sf2Ascii, true); - m_font = s_fonts[ relativePath ]; - - m_font->refCount++; - - m_fontId = fluid_synth_add_sfont( m_synth, m_font->fluidFont ); - } - - // Add to map, if doesn't exist. - else - { - bool loaded = false; - if( fluid_is_soundfont( sf2Ascii ) ) + if (fluid_synth_sfcount(m_synth) > 0) { - m_fontId = fluid_synth_sfload( m_synth, sf2Ascii, true ); - - if( fluid_synth_sfcount( m_synth ) > 0 ) - { - // Grab this sf from the top of the stack and add to list - m_font = new Sf2Font( fluid_synth_get_sfont( m_synth, 0 ) ); - s_fonts.insert( relativePath, m_font ); - loaded = true; - } + // Grab this sf from the top of the stack and add to list + m_font = fluid_synth_get_sfont(m_synth, 0); + loaded = true; } + } - if(!loaded) - { - collectErrorForUI( Sf2Instrument::tr( "A soundfont %1 could not be loaded." ). - arg( QFileInfo( _sf2File ).baseName() ) ); - } + if (!loaded) + { + collectErrorForUI(Sf2Instrument::tr("A soundfont %1 could not be loaded.").arg(QFileInfo(_sf2File).baseName())); } - s_fontsMutex.unlock(); m_synthMutex.unlock(); if( m_fontId >= 0 ) @@ -622,12 +582,12 @@ void Sf2Instrument::reloadSynth() { // Now, delete the old one and replace m_synthMutex.lock(); - fluid_synth_remove_sfont( m_synth, m_font->fluidFont ); + fluid_synth_remove_sfont( m_synth, m_font ); delete_fluid_synth( m_synth ); // New synth m_synth = new_fluid_synth( m_settings ); - m_fontId = fluid_synth_add_sfont( m_synth, m_font->fluidFont ); + m_fontId = fluid_synth_add_sfont( m_synth, m_font ); m_synthMutex.unlock(); // synth program change (set bank and patch) @@ -740,7 +700,7 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) 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_gen_set(voice.get(), GEN_COARSETUNE, voice.coarseTune() + detuning); fluid_voice_update_param(voice.get(), GEN_COARSETUNE); } } diff --git a/plugins/Sf2Player/Sf2Player.h b/plugins/Sf2Player/Sf2Player.h index bd7fa1b8161..17ddf550037 100644 --- a/plugins/Sf2Player/Sf2Player.h +++ b/plugins/Sf2Player/Sf2Player.h @@ -114,16 +114,12 @@ public slots: void updateTuning(); private: - static QMutex s_fontsMutex; - static QMap s_fonts; - static int (* s_origFree)( fluid_sfont_t * ); - SRC_STATE * m_srcState; fluid_settings_t* m_settings; fluid_synth_t* m_synth; - Sf2Font* m_font; + fluid_sfont_t* m_font; int m_fontId; QString m_filename; @@ -177,22 +173,6 @@ public slots: } ; - -// A soundfont in our font-map -class Sf2Font -{ - MM_OPERATORS -public: - Sf2Font( fluid_sfont_t * f ) : - fluidFont( f ), - refCount( 1 ) - {}; - - fluid_sfont_t * fluidFont; - int refCount; -}; - - namespace gui { diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt new file mode 100644 index 00000000000..49a80ca03aa --- /dev/null +++ b/plugins/SlicerT/CMakeLists.txt @@ -0,0 +1,10 @@ +INCLUDE(BuildPlugin) + +INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) +LINK_LIBRARIES(${FFTW3F_LIBRARIES}) + +INCLUDE_DIRECTORIES(${SAMPLERATE_INCLUDE_DIRS}) +LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) +LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) + +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTView.cpp SlicerTView.h SlicerTWaveform.cpp SlicerTWaveform.h MOCFILES SlicerT.h SlicerTView.h SlicerTWaveform.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp new file mode 100644 index 00000000000..2918265cead --- /dev/null +++ b/plugins/SlicerT/SlicerT.cpp @@ -0,0 +1,410 @@ +/* + * SlicerT.cpp - simple slicer plugin + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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 "SlicerT.h" + +#include +#include +#include + +#include "Engine.h" +#include "InstrumentTrack.h" +#include "PathUtil.h" +#include "Song.h" +#include "embed.h" +#include "lmms_constants.h" +#include "plugin_export.h" + +namespace lmms { + +extern "C" { +Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { + LMMS_STRINGIFY(PLUGIN_NAME), + "SlicerT", + QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), + "Daniel Kauss Serna ", + 0x0100, + Plugin::Type::Instrument, + new PluginPixmapLoader("logo"), + nullptr, + nullptr, +}; +} // end extern + +// ################################# SlicerT #################################### + +SlicerT::SlicerT(InstrumentTrack* instrumentTrack) + : Instrument(instrumentTrack, &slicert_plugin_descriptor) + , m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold")) + , m_fadeOutFrames(10.0f, 0.0f, 100.0f, 0.1f, this, tr("FadeOut")) + , m_originalBPM(1, 1, 999, this, tr("Original bpm")) + , m_sliceSnap(this, tr("Slice snap")) + , m_enableSync(false, this, tr("BPM sync")) + , m_originalSample() + , m_parentTrack(instrumentTrack) +{ + m_sliceSnap.addItem("Off"); + m_sliceSnap.addItem("1/1"); + m_sliceSnap.addItem("1/2"); + m_sliceSnap.addItem("1/4"); + m_sliceSnap.addItem("1/8"); + m_sliceSnap.addItem("1/16"); + m_sliceSnap.addItem("1/32"); + m_sliceSnap.setValue(0); +} + +void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) +{ + if (m_originalSample.frames() <= 1) { return; } + + int noteIndex = handle->key() - m_parentTrack->baseNote(); + const fpp_t frames = handle->framesLeftForCurrentPeriod(); + const f_cnt_t offset = handle->noteOffset(); + const int bpm = Engine::getSong()->getTempo(); + const float pitchRatio = 1 / std::exp2(m_parentTrack->pitchModel()->value() / 1200); + + float speedRatio = static_cast(m_originalBPM.value()) / bpm; + if (!m_enableSync.value()) { speedRatio = 1; } + speedRatio *= pitchRatio; + speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()); + + float sliceStart, sliceEnd; + if (noteIndex == 0) // full sample at base note + { + sliceStart = 0; + sliceEnd = 1; + } + else if (noteIndex > 0 && noteIndex < m_slicePoints.size()) + { + noteIndex -= 1; + sliceStart = m_slicePoints[noteIndex]; + sliceEnd = m_slicePoints[noteIndex + 1]; + } + else + { + emit isPlaying(-1, 0, 0); + return; + } + + if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); } + auto playbackState = static_cast(handle->m_pluginData); + + float noteDone = playbackState->noteDone(); + float noteLeft = sliceEnd - noteDone; + + if (noteLeft > 0) + { + int noteFrame = noteDone * m_originalSample.frames(); + + SRC_STATE* resampleState = playbackState->resamplingState(); + SRC_DATA resampleData; + resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); + resampleData.data_out = (workingBuffer + offset)->data(); + resampleData.input_frames = noteLeft * m_originalSample.frames(); + resampleData.output_frames = frames; + resampleData.src_ratio = speedRatio; + + src_process(resampleState, &resampleData); + + float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); + playbackState->setNoteDone(nextNoteDone); + + // exponential fade out, applyRelease() not used since it extends the note length + int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate(); + int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio; + for (int i = 0; i < frames; i++) + { + float fadeValue = static_cast(noteFramesLeft - i) / fadeOutFrames; + fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); + fadeValue = cosinusInterpolate(0, 1, fadeValue); + + workingBuffer[i + offset][0] *= fadeValue; + workingBuffer[i + offset][1] *= fadeValue; + } + + instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); + + emit isPlaying(noteDone, sliceStart, sliceEnd); + } + else { emit isPlaying(-1, 0, 0); } +} + +void SlicerT::deleteNotePluginData(NotePlayHandle* handle) +{ + delete static_cast(handle->m_pluginData); +} + +// uses the spectral flux to determine the change in magnitude +// resources: +// http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf +void SlicerT::findSlices() +{ + if (m_originalSample.frames() <= 1) { return; } + m_slicePoints = {}; + + const int windowSize = 512; + const float minBeatLength = 0.05f; // in seconds, ~ 1/4 length at 220 bpm + + int sampleRate = m_originalSample.sampleRate(); + int minDist = sampleRate * minBeatLength; + + float maxMag = -1; + std::vector singleChannel(m_originalSample.frames(), 0); + for (int i = 0; i < m_originalSample.frames(); i++) + { + singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; + maxMag = std::max(maxMag, singleChannel[i]); + } + + // normalize and find 0 crossings + std::vector zeroCrossings; + float lastValue = 1; + for (int i = 0; i < singleChannel.size(); i++) + { + singleChannel[i] /= maxMag; + if (sign(lastValue) != sign(singleChannel[i])) + { + zeroCrossings.push_back(i); + lastValue = singleChannel[i]; + } + } + + std::vector prevMags(windowSize / 2, 0); + std::vector fftIn(windowSize, 0); + std::array fftOut; + + fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut.data(), FFTW_MEASURE); + + int lastPoint = -minDist - 1; // to always store 0 first + float spectralFlux = 0; + float prevFlux = 1E-10; // small value, no divison by zero + float real, imag, magnitude, diff; + + for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) + { + // fft + std::copy_n(singleChannel.data() + i, windowSize, fftIn.data()); + fftwf_execute(fftPlan); + + // calculate spectral flux in regard to last window + for (int j = 0; j < windowSize / 2; j++) // only use niquistic frequencies + { + real = fftOut[j][0]; + imag = fftOut[j][1]; + magnitude = std::sqrt(real * real + imag * imag); + + // using L2-norm (euclidean distance) + diff = std::sqrt(std::pow(magnitude - prevMags[j], 2)); + spectralFlux += diff; + + prevMags[j] = magnitude; + } + + if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist) + { + m_slicePoints.push_back(i); + lastPoint = i; + if (m_slicePoints.size() > 128) { break; } // no more keys on the keyboard + } + + prevFlux = spectralFlux; + spectralFlux = 1E-10; // again for no divison by zero + } + + m_slicePoints.push_back(m_originalSample.frames()); + + for (float& sliceValue : m_slicePoints) + { + int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); + if (std::abs(sliceValue - closestZeroCrossing) < windowSize) { sliceValue = closestZeroCrossing; } + } + + float beatsPerMin = m_originalBPM.value() / 60.0f; + float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; + int noteSnap = m_sliceSnap.value(); + int sliceLock = samplesPerBeat / std::exp2(noteSnap + 1); + if (noteSnap == 0) { sliceLock = 1; } + for (float& sliceValue : m_slicePoints) + { + sliceValue += sliceLock / 2; + sliceValue -= static_cast(sliceValue) % sliceLock; + } + + m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end()); + + for (float& sliceIndex : m_slicePoints) + { + sliceIndex /= m_originalSample.frames(); + } + + m_slicePoints[0] = 0; + m_slicePoints[m_slicePoints.size() - 1] = 1; + + emit dataChanged(); +} + +// find the bpm of the sample by assuming its in 4/4 time signature , +// and lies in the 100 - 200 bpm range +void SlicerT::findBPM() +{ + if (m_originalSample.frames() <= 1) { return; } + + float sampleRate = m_originalSample.sampleRate(); + float totalFrames = m_originalSample.frames(); + float sampleLength = totalFrames / sampleRate; + + float bpmEstimate = 240.0f / sampleLength; + + while (bpmEstimate < 100) + { + bpmEstimate *= 2; + } + + while (bpmEstimate > 200) + { + bpmEstimate /= 2; + } + + m_originalBPM.setValue(bpmEstimate); + m_originalBPM.setInitValue(bpmEstimate); +} + +std::vector SlicerT::getMidi() +{ + std::vector outputNotes; + + float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); + float outFrames = m_originalSample.frames() * speedRatio; + + float framesPerTick = Engine::framesPerTick(); + float totalTicks = outFrames / framesPerTick; + float lastEnd = 0; + + for (int i = 0; i < m_slicePoints.size() - 1; i++) + { + float sliceStart = lastEnd; + float sliceEnd = totalTicks * m_slicePoints[i + 1]; + + Note sliceNote = Note(); + sliceNote.setKey(i + m_parentTrack->baseNote() + 1); + sliceNote.setPos(sliceStart); + sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 so that the notes allign + outputNotes.push_back(sliceNote); + + lastEnd = sliceEnd; + } + + return outputNotes; +} + +void SlicerT::updateFile(QString file) +{ + m_originalSample.setAudioFile(file); + + findBPM(); + findSlices(); + + emit dataChanged(); +} + +void SlicerT::updateSlices() +{ + findSlices(); +} + +void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) +{ + element.setAttribute("version", "1"); + element.setAttribute("src", m_originalSample.audioFile()); + if (m_originalSample.audioFile().isEmpty()) + { + QString s; + element.setAttribute("sampledata", m_originalSample.toBase64(s)); + } + + element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); + for (int i = 0; i < m_slicePoints.size(); i++) + { + element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]); + } + + m_fadeOutFrames.saveSettings(document, element, "fadeOut"); + m_noteThreshold.saveSettings(document, element, "threshold"); + m_originalBPM.saveSettings(document, element, "origBPM"); + m_enableSync.saveSettings(document, element, "syncEnable"); +} + +void SlicerT::loadSettings(const QDomElement& element) +{ + if (!element.attribute("src").isEmpty()) + { + m_originalSample.setAudioFile(element.attribute("src")); + + QString absolutePath = PathUtil::toAbsolute(m_originalSample.audioFile()); + if (!QFileInfo(absolutePath).exists()) + { + QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile()); + Engine::getSong()->collectError(message); + } + } + else if (!element.attribute("sampledata").isEmpty()) + { + m_originalSample.loadFromBase64(element.attribute("srcdata")); + } + + if (!element.attribute("totalSlices").isEmpty()) + { + int totalSlices = element.attribute("totalSlices").toInt(); + m_slicePoints = {}; + for (int i = 0; i < totalSlices; i++) + { + m_slicePoints.push_back(element.attribute(tr("slice_%1").arg(i)).toFloat()); + } + } + + m_fadeOutFrames.loadSettings(element, "fadeOut"); + m_noteThreshold.loadSettings(element, "threshold"); + m_originalBPM.loadSettings(element, "origBPM"); + m_enableSync.loadSettings(element, "syncEnable"); + + emit dataChanged(); +} + +QString SlicerT::nodeName() const +{ + return slicert_plugin_descriptor.name; +} + +gui::PluginView* SlicerT::instantiateView(QWidget* parent) +{ + return new gui::SlicerTView(this, parent); +} + +extern "C" { +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) +{ + return new SlicerT(static_cast(m)); +} +} // extern +} // namespace lmms diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h new file mode 100644 index 00000000000..8671eecd1f8 --- /dev/null +++ b/plugins/SlicerT/SlicerT.h @@ -0,0 +1,108 @@ +/* + * SlicerT.h - declaration of class SlicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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_SLICERT_H +#define LMMS_SLICERT_H + +#include +#include +#include + +#include "AutomatableModel.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "Note.h" +#include "SampleBuffer.h" +#include "SlicerTView.h" +#include "lmms_basics.h" + +namespace lmms { + +class PlaybackState +{ +public: + explicit PlaybackState(float startFrame) + : m_currentNoteDone(startFrame) + , m_resamplingState(src_new(SRC_LINEAR, DEFAULT_CHANNELS, nullptr)) + { + if (!m_resamplingState) { throw std::runtime_error{"Failed to create sample rate converter object"}; } + } + ~PlaybackState() noexcept { src_delete(m_resamplingState); } + + float noteDone() const { return m_currentNoteDone; } + void setNoteDone(float newNoteDone) { m_currentNoteDone = newNoteDone; } + + SRC_STATE* resamplingState() const { return m_resamplingState; } + +private: + float m_currentNoteDone; + SRC_STATE* m_resamplingState; +}; + +class SlicerT : public Instrument +{ + Q_OBJECT + +public slots: + void updateFile(QString file); + void updateSlices(); + +signals: + void isPlaying(float current, float start, float end); + +public: + SlicerT(InstrumentTrack* instrumentTrack); + + void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; + void deleteNotePluginData(NotePlayHandle* handle) override; + + void saveSettings(QDomDocument& document, QDomElement& element) override; + void loadSettings(const QDomElement& element) override; + + void findSlices(); + void findBPM(); + + QString nodeName() const override; + gui::PluginView* instantiateView(QWidget* parent) override; + + std::vector getMidi(); + +private: + FloatModel m_noteThreshold; + FloatModel m_fadeOutFrames; + IntModel m_originalBPM; + ComboBoxModel m_sliceSnap; + BoolModel m_enableSync; + + SampleBuffer m_originalSample; + + std::vector m_slicePoints; + + InstrumentTrack* m_parentTrack; + + friend class gui::SlicerTView; + friend class gui::SlicerTWaveform; +}; +} // namespace lmms +#endif // LMMS_SLICERT_H diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp new file mode 100644 index 00000000000..833d4b434af --- /dev/null +++ b/plugins/SlicerT/SlicerTView.cpp @@ -0,0 +1,193 @@ +/* + * SlicerTView.cpp - controls the UI for slicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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 "SlicerTView.h" + +#include +#include + +#include "Clipboard.h" +#include "DataFile.h" +#include "Engine.h" +#include "InstrumentTrack.h" +#include "SlicerT.h" +#include "Song.h" +#include "StringPairDrag.h" +#include "Track.h" +#include "embed.h" + +namespace lmms { + +namespace gui { + +SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) + : InstrumentViewFixedSize(instrument, parent) + , m_slicerTParent(instrument) +{ + // window settings + setAcceptDrops(true); + setAutoFillBackground(true); + + // render background + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + + m_wf = new SlicerTWaveform(248, 128, instrument, this); + m_wf->move(2, 6); + + m_snapSetting = new ComboBox(this, tr("Slice snap")); + m_snapSetting->setGeometry(185, 200, 55, ComboBox::DEFAULT_HEIGHT); + m_snapSetting->setToolTip(tr("Set slice snapping for detection")); + m_snapSetting->setModel(&m_slicerTParent->m_sliceSnap); + + m_syncToggle = new LedCheckBox("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green); + m_syncToggle->move(135, 187); + m_syncToggle->setToolTip(tr("Enable BPM sync")); + m_syncToggle->setModel(&m_slicerTParent->m_enableSync); + + m_bpmBox = new LcdSpinBox(3, "19purple", this); + m_bpmBox->move(130, 201); + m_bpmBox->setToolTip(tr("Original sample BPM")); + m_bpmBox->setModel(&m_slicerTParent->m_originalBPM); + + m_noteThresholdKnob = createStyledKnob(); + m_noteThresholdKnob->move(10, 197); + m_noteThresholdKnob->setToolTip(tr("Threshold used for slicing")); + m_noteThresholdKnob->setModel(&m_slicerTParent->m_noteThreshold); + + m_fadeOutKnob = createStyledKnob(); + m_fadeOutKnob->move(64, 197); + m_fadeOutKnob->setToolTip(tr("Fade Out per note in milliseconds")); + m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames); + + m_midiExportButton = new QPushButton(this); + m_midiExportButton->move(199, 150); + m_midiExportButton->setIcon(PLUGIN_NAME::getIconPixmap("copy_midi")); + m_midiExportButton->setToolTip(tr("Copy midi pattern to clipboard")); + connect(m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); + + m_resetButton = new QPushButton(this); + m_resetButton->move(18, 150); + m_resetButton->setIcon(PLUGIN_NAME::getIconPixmap("reset_slices")); + m_resetButton->setToolTip(tr("Reset Slices")); + connect(m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); +} + +Knob* SlicerTView::createStyledKnob() +{ + Knob* newKnob = new Knob(KnobType::Styled, this); + newKnob->setFixedSize(50, 40); + newKnob->setCenterPointX(24.0); + newKnob->setCenterPointY(15.0); + return newKnob; +} + +// copied from piano roll +void SlicerTView::exportMidi() +{ + using namespace Clipboard; + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + + DataFile dataFile(DataFile::Type::ClipboardData); + QDomElement noteList = dataFile.createElement("note-list"); + dataFile.content().appendChild(noteList); + + auto notes = m_slicerTParent->getMidi(); + if (notes.empty()) { return; } + + TimePos startPos(notes.front().pos().getBar(), 0); + for (Note& note : notes) + { + note.setPos(note.pos(startPos)); + note.saveState(dataFile, noteList); + } + + copyString(dataFile.toString(), MimeType::Default); +} + +void SlicerTView::openFiles() +{ + QString audioFile = m_slicerTParent->m_originalSample.openAudioFile(); + if (audioFile.isEmpty()) { return; } + m_slicerTParent->updateFile(audioFile); +} + +// all the drag stuff is copied from AudioFileProcessor +void SlicerTView::dragEnterEvent(QDragEnterEvent* dee) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + if (dee->mimeData()->hasFormat(mimeType(MimeType::StringPair))) + { + QString txt = dee->mimeData()->data(mimeType(MimeType::StringPair)); + if (txt.section(':', 0, 0) == QString("clip_%1").arg(static_cast(Track::Type::Sample))) + { + dee->acceptProposedAction(); + } + else if (txt.section(':', 0, 0) == "samplefile") { dee->acceptProposedAction(); } + else { dee->ignore(); } + } + else { dee->ignore(); } +} + +void SlicerTView::dropEvent(QDropEvent* de) +{ + QString type = StringPairDrag::decodeKey(de); + QString value = StringPairDrag::decodeValue(de); + if (type == "samplefile") + { + // set m_wf wave file + m_slicerTParent->updateFile(value); + return; + } + else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) + { + DataFile dataFile(value.toUtf8()); + m_slicerTParent->updateFile(dataFile.content().firstChild().toElement().attribute("src")); + de->accept(); + return; + } + + de->ignore(); +} + +void SlicerTView::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); + brush.setPen(QColor(255, 255, 255)); + brush.setFont(QFont(brush.font().family(), 7, -1, false)); + + brush.drawText(8, s_topTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Reset")); + brush.drawText(188, s_topTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Midi")); + + brush.drawText(8, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Threshold")); + brush.drawText(63, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Fade Out")); + brush.drawText(127, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("BPM")); + brush.drawText(188, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Snap")); +} + +} // namespace gui +} // namespace lmms diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h new file mode 100644 index 00000000000..ea2b979fc42 --- /dev/null +++ b/plugins/SlicerT/SlicerTView.h @@ -0,0 +1,85 @@ +/* + * SlicerTView.h - declaration of class SlicerTView + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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_GUI_SLICERT_VIEW_H +#define LMMS_GUI_SLICERT_VIEW_H + +#include + +#include "ComboBox.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "Knob.h" +#include "LcdSpinBox.h" +#include "LedCheckBox.h" +#include "PixmapButton.h" +#include "SlicerTWaveform.h" + +namespace lmms { + +class SlicerT; + +namespace gui { + +class SlicerTView : public InstrumentViewFixedSize +{ + Q_OBJECT + +public slots: + void exportMidi(); + void openFiles(); + +public: + SlicerTView(SlicerT* instrument, QWidget* parent); + + static constexpr int s_textBoxHeight = 20; + static constexpr int s_textBoxWidth = 50; + static constexpr int s_topTextY = 170; + static constexpr int s_bottomTextY = 220; + +protected: + virtual void dragEnterEvent(QDragEnterEvent* dee); + virtual void dropEvent(QDropEvent* de); + + virtual void paintEvent(QPaintEvent* pe); + +private: + SlicerT* m_slicerTParent; + + Knob* m_noteThresholdKnob; + Knob* m_fadeOutKnob; + LcdSpinBox* m_bpmBox; + ComboBox* m_snapSetting; + LedCheckBox* m_syncToggle; + + QPushButton* m_resetButton; + QPushButton* m_midiExportButton; + + SlicerTWaveform* m_wf; + + Knob* createStyledKnob(); +}; +} // namespace gui +} // namespace lmms +#endif // LMMS_GUI_SLICERT_VIEW_H diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp new file mode 100644 index 00000000000..6685f4f8cec --- /dev/null +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -0,0 +1,418 @@ +/* + * SlicerTWaveform.cpp - slice editor for SlicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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 "SlicerTWaveform.h" + +#include + +#include "SlicerT.h" +#include "SlicerTView.h" +#include "embed.h" + +namespace lmms { + +namespace gui { + +static QColor s_emptyColor = QColor(0, 0, 0, 0); +static QColor s_waveformColor = QColor(123, 49, 212); +static QColor s_waveformBgColor = QColor(255, 255, 255, 0); +static QColor s_waveformMaskColor = QColor(151, 65, 255); // update this if s_waveformColor changes +static QColor s_waveformInnerColor = QColor(183, 124, 255); + +static QColor s_playColor = QColor(255, 255, 255, 200); +static QColor s_playHighlightColor = QColor(255, 255, 255, 70); + +static QColor s_sliceColor = QColor(218, 193, 255); +static QColor s_sliceShadowColor = QColor(136, 120, 158); +static QColor s_sliceHighlightColor = QColor(255, 255, 255); + +static QColor s_seekerColor = QColor(178, 115, 255); +static QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); +static QColor s_seekerShadowColor = QColor(0, 0, 0, 120); + +SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent) + : QWidget(parent) + , m_width(totalWidth) + , m_height(totalHeight) + , m_seekerWidth(totalWidth - s_seekerHorMargin * 2) + , m_editorHeight(totalHeight - s_seekerHeight - s_middleMargin) + , m_editorWidth(totalWidth) + , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slice_indicator_arrow")) + , m_seeker(QPixmap(m_seekerWidth, s_seekerHeight)) + , m_seekerWaveform(QPixmap(m_seekerWidth, s_seekerHeight)) + , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) + , m_sliceEditor(QPixmap(totalWidth, m_editorHeight)) + , m_emptySampleIcon(embed::getIconPixmap("sample_track")) + , m_slicerTParent(instrument) +{ + setFixedSize(m_width, m_height); + setMouseTracking(true); + + m_seekerWaveform.fill(s_waveformBgColor); + m_editorWaveform.fill(s_waveformBgColor); + + connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying); + connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI); + + m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor); + + m_updateTimer.start(); + updateUI(); +} + +void SlicerTWaveform::drawSeekerWaveform() +{ + m_seekerWaveform.fill(s_waveformBgColor); + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + QPainter brush(&m_seekerWaveform); + brush.setPen(s_waveformColor); + + m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), + 0, m_slicerTParent->m_originalSample.frames()); + + // increase brightness in inner color + QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); + brush.setPen(s_waveformInnerColor); + brush.drawPixmap(0, 0, innerMask); +} + +void SlicerTWaveform::drawSeeker() +{ + m_seeker.fill(s_emptyColor); + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + QPainter brush(&m_seeker); + + brush.setPen(s_sliceColor); + for (float sliceValue : m_slicerTParent->m_slicePoints) + { + float xPos = sliceValue * m_seekerWidth; + brush.drawLine(xPos, 0, xPos, s_seekerHeight); + } + + float seekerStartPosX = m_seekerStart * m_seekerWidth; + float seekerEndPosX = m_seekerEnd * m_seekerWidth; + float seekerMiddleWidth = (m_seekerEnd - m_seekerStart) * m_seekerWidth; + + float noteCurrentPosX = m_noteCurrent * m_seekerWidth; + float noteStartPosX = m_noteStart * m_seekerWidth; + float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth; + + brush.setPen(s_playColor); + brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, s_seekerHeight); + brush.fillRect(noteStartPosX, 0, noteEndPosX, s_seekerHeight, s_playHighlightColor); + + brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight, s_seekerHighlightColor); + + brush.fillRect(0, 0, seekerStartPosX, s_seekerHeight, s_seekerShadowColor); + brush.fillRect(seekerEndPosX - 1, 0, m_seekerWidth, s_seekerHeight, s_seekerShadowColor); + + brush.setPen(QPen(s_seekerColor, 1)); + brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight - 1); // -1 needed +} + +void SlicerTWaveform::drawEditorWaveform() +{ + m_editorWaveform.fill(s_emptyColor); + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + + QPainter brush(&m_editorWaveform); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + + brush.setPen(s_waveformColor); + float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; + m_slicerTParent->m_originalSample.visualize( + brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); + + // increase brightness in inner color + QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); + brush.setPen(s_waveformInnerColor); + brush.drawPixmap(0, 0, innerMask); +} + +void SlicerTWaveform::drawEditor() +{ + m_sliceEditor.fill(s_waveformBgColor); + QPainter brush(&m_sliceEditor); + + // No sample loaded + if (m_slicerTParent->m_originalSample.frames() <= 1) + { + brush.setPen(s_playHighlightColor); + brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); + brush.drawText( + m_editorWidth / 2 - 100, m_editorHeight / 2 - 110, 200, 200, Qt::AlignCenter, tr("Click to load sample")); + int iconOffsetX = m_emptySampleIcon.width() / 2.0f; + int iconOffsetY = m_emptySampleIcon.height() / 2.0f - 13; + brush.drawPixmap(m_editorWidth / 2.0f - iconOffsetX, m_editorHeight / 2.0f - iconOffsetY, m_emptySampleIcon); + return; + } + + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + float numFramesToDraw = endFrame - startFrame; + + // playback state + float noteCurrentPos = (m_noteCurrent - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + float noteStartPos = (m_noteStart - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + float noteLength = (m_noteEnd - m_noteStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + + brush.setPen(s_playHighlightColor); + brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2); + + brush.drawPixmap(0, 0, m_editorWaveform); + + brush.setPen(s_playColor); + brush.drawLine(noteCurrentPos, 0, noteCurrentPos, m_editorHeight); + brush.fillRect(noteStartPos, 0, noteLength, m_editorHeight, s_playHighlightColor); + + brush.setPen(QPen(s_sliceColor, 2)); + + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) + { + float xPos = (m_slicerTParent->m_slicePoints.at(i) - startFrame) / numFramesToDraw * m_editorWidth; + + if (i == m_closestSlice) + { + brush.setPen(QPen(s_sliceHighlightColor, 2)); + brush.drawLine(xPos, 0, xPos, m_editorHeight); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + continue; + } + else + { + brush.setPen(QPen(s_sliceShadowColor, 1)); + brush.drawLine(xPos - 1, 0, xPos - 1, m_editorHeight); + brush.setPen(QPen(s_sliceColor, 1)); + brush.drawLine(xPos, 0, xPos, m_editorHeight); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + } + } +} + +void SlicerTWaveform::isPlaying(float current, float start, float end) +{ + if (!m_updateTimer.hasExpired(s_minMilisPassed)) { return; } + m_noteCurrent = current; + m_noteStart = start; + m_noteEnd = end; + drawSeeker(); + drawEditor(); + update(); + m_updateTimer.restart(); +} + +// this should only be called if one of the waveforms has to update +void SlicerTWaveform::updateUI() +{ + drawSeekerWaveform(); + drawEditorWaveform(); + drawSeeker(); + drawEditor(); + update(); +} + +// updates the closest object and changes the cursor respectivly +void SlicerTWaveform::updateClosest(QMouseEvent* me) +{ + float normalizedClickSeeker = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; + + m_closestObject = UIObjects::Nothing; + m_closestSlice = -1; + + if (me->y() < s_seekerHeight) + { + if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) + { + m_closestObject = UIObjects::SeekerStart; + } + else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) + { + m_closestObject = UIObjects::SeekerEnd; + } + else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) + { + m_closestObject = UIObjects::SeekerMiddle; + } + } + else + { + m_closestSlice = -1; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) + { + float sliceIndex = m_slicerTParent->m_slicePoints.at(i); + float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); + + if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick) + { + m_closestObject = UIObjects::SlicePoint; + m_closestSlice = i; + } + } + } + updateCursor(); + drawSeeker(); + drawEditor(); + update(); +} + +void SlicerTWaveform::updateCursor() +{ + if (m_closestObject == UIObjects::SlicePoint || m_closestObject == UIObjects::SeekerStart + || m_closestObject == UIObjects::SeekerEnd) + { + setCursor(Qt::SizeHorCursor); + } + else if (m_closestObject == UIObjects::SeekerMiddle && m_seekerEnd - m_seekerStart != 1.0f) + { + setCursor(Qt::SizeAllCursor); + } + else { setCursor(Qt::ArrowCursor); } +} + +// handles deletion, reset and middles seeker +void SlicerTWaveform::mousePressEvent(QMouseEvent* me) +{ + switch (me->button()) + { + case Qt::MouseButton::MiddleButton: + m_seekerStart = 0; + m_seekerEnd = 1; + m_zoomLevel = 1; + drawEditorWaveform(); + break; + case Qt::MouseButton::LeftButton: + if (m_slicerTParent->m_originalSample.frames() <= 1) { static_cast(parent())->openFiles(); } + // update seeker middle for correct movement + m_seekerMiddle = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + break; + case Qt::MouseButton::RightButton: + if (m_slicerTParent->m_slicePoints.size() > 2 && m_closestObject == UIObjects::SlicePoint) + { + m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); + } + break; + default:; + } + updateClosest(me); +} + +// sort slices after moving and remove draggable object +void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) +{ + std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); + updateClosest(me); +} + +// this handles dragging and mouse cursor changes +// what is being dragged is determined in mousePressEvent +void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) +{ + // if no button pressed, update closest and cursor + if (me->buttons() == Qt::MouseButton::NoButton) + { + updateClosest(me); + return; + } + + float normalizedClickSeeker = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; + + float distStart = m_seekerStart - m_seekerMiddle; + float distEnd = m_seekerEnd - m_seekerMiddle; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + + switch (m_closestObject) + { + case UIObjects::SeekerStart: + m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - s_minSeekerDistance); + drawEditorWaveform(); + break; + + case UIObjects::SeekerEnd: + m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + s_minSeekerDistance, 1.0f); + drawEditorWaveform(); + break; + + case UIObjects::SeekerMiddle: + m_seekerMiddle = normalizedClickSeeker; + + if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) + { + m_seekerStart = m_seekerMiddle + distStart; + m_seekerEnd = m_seekerMiddle + distEnd; + } + drawEditorWaveform(); + break; + + case UIObjects::SlicePoint: + if (m_closestSlice == -1) { break; } + m_slicerTParent->m_slicePoints.at(m_closestSlice) + = startFrame + normalizedClickEditor * (endFrame - startFrame); + m_slicerTParent->m_slicePoints.at(m_closestSlice) + = std::clamp(m_slicerTParent->m_slicePoints.at(m_closestSlice), 0.0f, 1.0f); + break; + case UIObjects::Nothing: + break; + } + // dont update closest, and update seeker waveform + drawSeeker(); + drawEditor(); + update(); +} + +void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) +{ + if (me->button() != Qt::MouseButton::LeftButton) { return; } + + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); + + m_slicerTParent->m_slicePoints.insert(m_slicerTParent->m_slicePoints.begin(), slicePosition); + std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); +} + +void SlicerTWaveform::wheelEvent(QWheelEvent* we) +{ + m_zoomLevel += we->angleDelta().y() / 360.0f * s_zoomSensitivity; + m_zoomLevel = std::max(0.0f, m_zoomLevel); + + updateUI(); +} + +void SlicerTWaveform::paintEvent(QPaintEvent* pe) +{ + QPainter p(this); + p.drawPixmap(s_seekerHorMargin, 0, m_seekerWaveform); + p.drawPixmap(s_seekerHorMargin, 0, m_seeker); + p.drawPixmap(0, s_seekerHeight + s_middleMargin, m_sliceEditor); +} +} // namespace gui +} // namespace lmms diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h new file mode 100644 index 00000000000..6478e7f8684 --- /dev/null +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -0,0 +1,125 @@ +/* + * SlicerTWaveform.h - declaration of class SlicerTWaveform + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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_GUI_SLICERT_WAVEFORM_H +#define LMMS_GUI_SLICERT_WAVEFORM_H + +#include +#include +#include +#include +#include +#include + +#include "Instrument.h" +#include "SampleBuffer.h" + +namespace lmms { + +class SlicerT; + +namespace gui { + +class SlicerTWaveform : public QWidget +{ + Q_OBJECT + +public slots: + void updateUI(); + void isPlaying(float current, float start, float end); + +public: + SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent); + + // predefined sizes + static constexpr int s_seekerHorMargin = 5; + static constexpr int s_seekerHeight = 38; // used to calcualte all vertical sizes + static constexpr int s_middleMargin = 6; + + // interaction behavior values + static constexpr float s_distanceForClick = 0.02f; + static constexpr float s_minSeekerDistance = 0.13f; + static constexpr float s_zoomSensitivity = 0.5f; + static constexpr int s_minMilisPassed = 10; + + enum class UIObjects + { + Nothing, + SeekerStart, + SeekerEnd, + SeekerMiddle, + SlicePoint, + }; + +protected: + void mousePressEvent(QMouseEvent* me) override; + void mouseReleaseEvent(QMouseEvent* me) override; + void mouseMoveEvent(QMouseEvent* me) override; + void mouseDoubleClickEvent(QMouseEvent* me) override; + void wheelEvent(QWheelEvent* we) override; + + void paintEvent(QPaintEvent* pe) override; + +private: + int m_width; + int m_height; + + int m_seekerWidth; + int m_editorHeight; + int m_editorWidth; + + UIObjects m_closestObject; + int m_closestSlice = -1; + + float m_seekerStart = 0; + float m_seekerEnd = 1; + float m_seekerMiddle = 0.5f; + + float m_noteCurrent; + float m_noteStart; + float m_noteEnd; + + float m_zoomLevel = 1.0f; + + QPixmap m_sliceArrow; + QPixmap m_seeker; + QPixmap m_seekerWaveform; + QPixmap m_editorWaveform; + QPixmap m_sliceEditor; + QPixmap m_emptySampleIcon; + + SlicerT* m_slicerTParent; + + QElapsedTimer m_updateTimer; + void drawSeekerWaveform(); + void drawSeeker(); + void drawEditorWaveform(); + void drawEditor(); + + void updateClosest(QMouseEvent* me); + void updateCursor(); +}; +} // namespace gui +} // namespace lmms +#endif // LMMS_GUI_SLICERT_WAVEFORM_H diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png new file mode 100644 index 00000000000..e166273c705 Binary files /dev/null and b/plugins/SlicerT/artwork.png differ diff --git a/plugins/SlicerT/copy_midi.png b/plugins/SlicerT/copy_midi.png new file mode 100644 index 00000000000..e2ef15199bc Binary files /dev/null and b/plugins/SlicerT/copy_midi.png differ diff --git a/plugins/SlicerT/logo.png b/plugins/SlicerT/logo.png new file mode 100644 index 00000000000..f2c4fabf14f Binary files /dev/null and b/plugins/SlicerT/logo.png differ diff --git a/plugins/SlicerT/reset_slices.png b/plugins/SlicerT/reset_slices.png new file mode 100644 index 00000000000..15de6cdc18f Binary files /dev/null and b/plugins/SlicerT/reset_slices.png differ diff --git a/plugins/SlicerT/slice_indicator_arrow.png b/plugins/SlicerT/slice_indicator_arrow.png new file mode 100644 index 00000000000..1f60fc3bfcc Binary files /dev/null and b/plugins/SlicerT/slice_indicator_arrow.png differ diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index eb09c793a9f..d36d8a3eeb2 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -89,28 +89,28 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // pause and freeze buttons auto pauseButton = new PixmapButton(this, tr("Pause")); pauseButton->setToolTip(tr("Pause data acquisition")); - auto pauseOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("play").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto pauseOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("pause").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - pauseOnPixmap->setDevicePixelRatio(devicePixelRatio()); - pauseOffPixmap->setDevicePixelRatio(devicePixelRatio()); - pauseButton->setActiveGraphic(*pauseOnPixmap); - pauseButton->setInactiveGraphic(*pauseOffPixmap); + static auto s_pauseOnPixmap + = PLUGIN_NAME::getIconPixmap("play").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_pauseOffPixmap + = PLUGIN_NAME::getIconPixmap("pause").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_pauseOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_pauseOffPixmap.setDevicePixelRatio(devicePixelRatio()); + pauseButton->setActiveGraphic(s_pauseOnPixmap); + pauseButton->setInactiveGraphic(s_pauseOffPixmap); pauseButton->setCheckable(true); pauseButton->setModel(&controls->m_pauseModel); config_layout->addWidget(pauseButton, 0, 0, 2, 1, Qt::AlignHCenter); auto refFreezeButton = new PixmapButton(this, tr("Reference freeze")); refFreezeButton->setToolTip(tr("Freeze current input as a reference / disable falloff in peak-hold mode.")); - auto freezeOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("freeze").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto freezeOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("freeze_off").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - freezeOnPixmap->setDevicePixelRatio(devicePixelRatio()); - freezeOffPixmap->setDevicePixelRatio(devicePixelRatio()); - refFreezeButton->setActiveGraphic(*freezeOnPixmap); - refFreezeButton->setInactiveGraphic(*freezeOffPixmap); + static auto s_freezeOnPixmap + = PLUGIN_NAME::getIconPixmap("freeze").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_freezeOffPixmap + = PLUGIN_NAME::getIconPixmap("freeze_off").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_freezeOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_freezeOffPixmap.setDevicePixelRatio(devicePixelRatio()); + refFreezeButton->setActiveGraphic(s_freezeOnPixmap); + refFreezeButton->setInactiveGraphic(s_freezeOffPixmap); refFreezeButton->setCheckable(true); refFreezeButton->setModel(&controls->m_refFreezeModel); config_layout->addWidget(refFreezeButton, 2, 0, 2, 1, Qt::AlignHCenter); @@ -147,14 +147,14 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // frequency: linear / log. switch and range selector auto logXButton = new PixmapButton(this, tr("Logarithmic frequency")); logXButton->setToolTip(tr("Switch between logarithmic and linear frequency scale")); - auto logXOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("x_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto logXOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("x_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - logXOnPixmap->setDevicePixelRatio(devicePixelRatio()); - logXOffPixmap->setDevicePixelRatio(devicePixelRatio()); - logXButton->setActiveGraphic(*logXOnPixmap); - logXButton->setInactiveGraphic(*logXOffPixmap); + static auto s_logXOnPixmap + = PLUGIN_NAME::getIconPixmap("x_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_logXOffPixmap + = PLUGIN_NAME::getIconPixmap("x_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_logXOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_logXOffPixmap.setDevicePixelRatio(devicePixelRatio()); + logXButton->setActiveGraphic(s_logXOnPixmap); + logXButton->setInactiveGraphic(s_logXOffPixmap); logXButton->setCheckable(true); logXButton->setModel(&controls->m_logXModel); config_layout->addWidget(logXButton, 0, 2, 2, 1, Qt::AlignRight); @@ -169,14 +169,14 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // amplitude: linear / log switch and range selector auto logYButton = new PixmapButton(this, tr("Logarithmic amplitude")); logYButton->setToolTip(tr("Switch between logarithmic and linear amplitude scale")); - auto logYOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("y_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto logYOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("y_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - logYOnPixmap->setDevicePixelRatio(devicePixelRatio()); - logYOffPixmap->setDevicePixelRatio(devicePixelRatio()); - logYButton->setActiveGraphic(*logYOnPixmap); - logYButton->setInactiveGraphic(*logYOffPixmap); + static auto s_logYOnPixmap + = PLUGIN_NAME::getIconPixmap("y_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_logYOffPixmap + = PLUGIN_NAME::getIconPixmap("y_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_logYOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_logYOffPixmap.setDevicePixelRatio(devicePixelRatio()); + logYButton->setActiveGraphic(s_logYOnPixmap); + logYButton->setInactiveGraphic(s_logYOffPixmap); logYButton->setCheckable(true); logYButton->setModel(&controls->m_logYModel); config_layout->addWidget(logYButton, 2, 2, 2, 1, Qt::AlignRight); @@ -190,9 +190,9 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // FFT: block size: icon and selector auto blockSizeLabel = new QLabel("", this); - auto blockSizeIcon = new QPixmap(PLUGIN_NAME::getIconPixmap("block_size")); - blockSizeIcon->setDevicePixelRatio(devicePixelRatio()); - blockSizeLabel->setPixmap(blockSizeIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + static auto s_blockSizeIcon = PLUGIN_NAME::getIconPixmap("block_size"); + s_blockSizeIcon.setDevicePixelRatio(devicePixelRatio()); + blockSizeLabel->setPixmap(s_blockSizeIcon.scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); config_layout->addWidget(blockSizeLabel, 0, 4, 2, 1, Qt::AlignRight); auto blockSizeCombo = new ComboBox(this, tr("FFT block size")); @@ -206,9 +206,9 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // FFT: window type: icon and selector auto windowLabel = new QLabel("", this); - auto windowIcon = new QPixmap(PLUGIN_NAME::getIconPixmap("window")); - windowIcon->setDevicePixelRatio(devicePixelRatio()); - windowLabel->setPixmap(windowIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + static auto s_windowIcon = PLUGIN_NAME::getIconPixmap("window"); + s_windowIcon.setDevicePixelRatio(devicePixelRatio()); + windowLabel->setPixmap(s_windowIcon.scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); config_layout->addWidget(windowLabel, 2, 4, 2, 1, Qt::AlignRight); auto windowCombo = new ComboBox(this, tr("FFT window type")); @@ -307,14 +307,14 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // Advanced settings button auto advancedButton = new PixmapButton(this, tr("Advanced settings")); advancedButton->setToolTip(tr("Access advanced settings")); - auto advancedOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_on") - .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto advancedOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_off") - .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - advancedOnPixmap->setDevicePixelRatio(devicePixelRatio()); - advancedOffPixmap->setDevicePixelRatio(devicePixelRatio()); - advancedButton->setActiveGraphic(*advancedOnPixmap); - advancedButton->setInactiveGraphic(*advancedOffPixmap); + static auto s_advancedOnPixmap = PLUGIN_NAME::getIconPixmap("advanced_on") + .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_advancedOffPixmap = PLUGIN_NAME::getIconPixmap("advanced_off") + .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_advancedOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_advancedOffPixmap.setDevicePixelRatio(devicePixelRatio()); + advancedButton->setActiveGraphic(s_advancedOnPixmap); + advancedButton->setInactiveGraphic(s_advancedOffPixmap); advancedButton->setCheckable(true); controls_layout->addStretch(0); controls_layout->addWidget(advancedButton); diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index b746e949120..dd3b094940f 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -87,6 +87,7 @@ MalletsInstrument::MalletsInstrument( InstrumentTrack * _instrument_track ): m_strikeModel( true, this, tr( "Bowed" ) ), m_presetsModel(this), m_spreadModel(0, 0, 255, 1, this, tr( "Spread" )), + m_randomModel(0.0f, 0.0f, 1.0f, 0.01f, this, tr("Randomness")), m_versionModel( MALLETS_PRESET_VERSION, 0, MALLETS_PRESET_VERSION, this, "" ), m_isOldVersionModel( false, this, "" ), m_filesMissing( !QDir( ConfigManager::inst()->stkDir() ).exists() || @@ -155,6 +156,7 @@ void MalletsInstrument::saveSettings( QDomDocument & _doc, QDomElement & _this ) m_presetsModel.saveSettings( _doc, _this, "preset" ); m_spreadModel.saveSettings( _doc, _this, "spread" ); + m_randomModel.saveSettings(_doc, _this, "randomness"); m_versionModel.saveSettings( _doc, _this, "version" ); m_isOldVersionModel.saveSettings( _doc, _this, "oldversion" ); } @@ -189,6 +191,7 @@ void MalletsInstrument::loadSettings( const QDomElement & _this ) m_presetsModel.loadSettings( _this, "preset" ); m_spreadModel.loadSettings( _this, "spread" ); + m_randomModel.loadSettings(_this, "randomness"); m_isOldVersionModel.loadSettings( _this, "oldversion" ); // To maintain backward compatibility @@ -284,7 +287,7 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, } int p = m_presetsModel.value(); - + const float freq = _n->frequency(); if (!_n->m_pluginData) { @@ -293,6 +296,39 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, m_isOldVersionModel.value() ? 100.0 : 200.0; const float vel = _n->getVolume() / velocityAdjust; + const float random = m_randomModel.value(); + float hardness = m_hardnessModel.value(); + float position = m_positionModel.value(); + float modulator = m_modulatorModel.value(); + float crossfade = m_crossfadeModel.value(); + float pressure = m_pressureModel.value(); + float speed = m_velocityModel.value(); + + if (p < 9) + { + hardness += random * (static_cast(fast_rand() % 128) - 64.0); + hardness = std::clamp(hardness, 0.0f, 128.0f); + + position += random * (static_cast(fast_rand() % 64) - 32.0); + position = std::clamp(position, 0.0f, 64.0f); + } + else if (p == 9) + { + modulator += random * (static_cast(fast_rand() % 128) - 64.0); + modulator = std::clamp(modulator, 0.0f, 128.0f); + + crossfade += random * (static_cast(fast_rand() % 128) - 64.0); + crossfade = std::clamp(crossfade, 0.0f, 128.0f); + } + else + { + pressure += random * (static_cast(fast_rand() % 128) - 64.0); + pressure = std::clamp(pressure, 0.0f, 128.0f); + + speed += random * (static_cast(fast_rand() % 128) - 64.0); + speed = std::clamp(speed, 0.0f, 128.0f); + } + // critical section as STK is not thread-safe static QMutex m; m.lock(); @@ -301,8 +337,8 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, _n->m_pluginData = new MalletsSynth( freq, vel, m_stickModel.value(), - m_hardnessModel.value(), - m_positionModel.value(), + hardness, + position, m_vibratoGainModel.value(), m_vibratoFreqModel.value(), p, @@ -315,8 +351,8 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, vel, p, m_lfoDepthModel.value(), - m_modulatorModel.value(), - m_crossfadeModel.value(), + modulator, + crossfade, m_lfoSpeedModel.value(), m_adsrModel.value(), (uint8_t) m_spreadModel.value(), @@ -326,12 +362,12 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, { _n->m_pluginData = new MalletsSynth( freq, vel, - m_pressureModel.value(), + pressure, m_motionModel.value(), m_vibratoModel.value(), p - 10, m_strikeModel.value() * 128.0, - m_velocityModel.value(), + speed, (uint8_t) m_spreadModel.value(), Engine::audioEngine()->processingSampleRate() ); } @@ -343,8 +379,20 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, const f_cnt_t offset = _n->noteOffset(); auto ps = static_cast(_n->m_pluginData); - ps->setFrequency( freq ); + ps->setFrequency(freq); + p = ps->presetIndex(); + if (p < 9) // ModalBar updates + { + ps->setVibratoGain(m_vibratoGainModel.value()); + ps->setVibratoFreq(m_vibratoFreqModel.value()); + } + else if (p == 9) // Tubular Bells updates + { + ps->setADSR(m_adsrModel.value()); + ps->setLFODepth(m_lfoDepthModel.value()); + ps->setLFOSpeed(m_lfoSpeedModel.value()); + } sample_t add_scale = 0.0f; if( p == 10 && m_isOldVersionModel.value() == true ) @@ -412,6 +460,11 @@ MalletsInstrumentView::MalletsInstrumentView( MalletsInstrument * _instrument, m_spreadKnob->move( 190, 140 ); m_spreadKnob->setHintText( tr( "Spread:" ), "" ); + m_randomKnob = new Knob(KnobType::Vintage32, this); + m_randomKnob->setLabel(tr("Random")); + m_randomKnob->move(190, 190); + m_randomKnob->setHintText(tr("Random:"), ""); + // try to inform user about missing Stk-installation if( _instrument->m_filesMissing && getGUI() != nullptr ) { @@ -467,7 +520,7 @@ QWidget * MalletsInstrumentView::setupModalBarControls( QWidget * _parent ) m_stickKnob->setLabel( tr( "Stick mix" ) ); m_stickKnob->move( 190, 90 ); m_stickKnob->setHintText( tr( "Stick mix:" ), "" ); - + return( widget ); } @@ -565,6 +618,7 @@ void MalletsInstrumentView::modelChanged() // m_strikeLED->setModel( &inst->m_strikeModel ); m_presetsCombo->setModel( &inst->m_presetsModel ); m_spreadKnob->setModel( &inst->m_spreadModel ); + m_randomKnob->setModel(&inst->m_randomModel); } diff --git a/plugins/Stk/Mallets/Mallets.h b/plugins/Stk/Mallets/Mallets.h index f66ac25d011..91e2dfce140 100644 --- a/plugins/Stk/Mallets/Mallets.h +++ b/plugins/Stk/Mallets/Mallets.h @@ -124,12 +124,38 @@ class MalletsSynth return( s ); } - inline void setFrequency( const StkFloat _pitch ) + inline void setFrequency(const StkFloat _pitch) { - if( m_voice ) - { - m_voice->setFrequency( _pitch ); - } + if (m_voice) { m_voice->setFrequency(_pitch); } + } + + // ModalBar updates + inline void setVibratoGain(const StkFloat _control8) + { + // bug in stk, Control Number 8 and 1 swapped in ModalBar + // we send the control number for stick direct mix instead + if (m_voice) { m_voice->controlChange(8, _control8); } + } + + inline void setVibratoFreq(const StkFloat _control11) + { + if (m_voice) { m_voice->controlChange(11, _control11); } + } + + // Tubular Bells updates + inline void setADSR(const StkFloat _control128) + { + if (m_voice) { m_voice->controlChange(128, _control128); } + } + + inline void setLFODepth(const StkFloat _control1) + { + if (m_voice) { m_voice->controlChange(1, _control1); } + } + + inline void setLFOSpeed(const StkFloat _control11) + { + if (m_voice) { m_voice->controlChange(11, _control11); } } inline int presetIndex() @@ -197,6 +223,7 @@ class MalletsInstrument : public Instrument ComboBoxModel m_presetsModel; FloatModel m_spreadModel; + FloatModel m_randomModel; IntModel m_versionModel; BoolModel m_isOldVersionModel; @@ -255,6 +282,7 @@ public slots: ComboBox * m_presetsCombo; Knob * m_spreadKnob; + Knob * m_randomKnob; }; diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index a696a4b2ded..583075c0cd3 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -485,21 +485,11 @@ gui::PluginView * VestigeInstrument::instantiateView( QWidget * _parent ) namespace gui { -QPixmap * VestigeInstrumentView::s_artwork = nullptr; -QPixmap * ManageVestigeInstrumentView::s_artwork = nullptr; - - VestigeInstrumentView::VestigeInstrumentView( Instrument * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ), lastPosInMenu (0) { - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( - "artwork" ) ); - } - m_openPluginButton = new PixmapButton( this, "" ); m_openPluginButton->setCheckable( false ); m_openPluginButton->setCursor( Qt::PointingHandCursor ); @@ -881,7 +871,8 @@ void VestigeInstrumentView::paintEvent( QPaintEvent * ) { QPainter p( this ); - p.drawPixmap( 0, 0, *s_artwork ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + p.drawPixmap(0, 0, s_artwork); QString plugin_name = ( m_vi->m_plugin != nullptr ) ? m_vi->m_plugin->name()/* + QString::number( diff --git a/plugins/Vestige/Vestige.h b/plugins/Vestige/Vestige.h index f740913ead5..9ac66f74da6 100644 --- a/plugins/Vestige/Vestige.h +++ b/plugins/Vestige/Vestige.h @@ -131,8 +131,6 @@ protected slots: private: - static QPixmap * s_artwork; - VestigeInstrument * m_vi; QWidget *widget; @@ -175,7 +173,6 @@ protected slots: private: virtual void modelChanged(); - static QPixmap * s_artwork; VestigeInstrument * m_vi; diff --git a/plugins/VstEffect/VstEffectControls.cpp b/plugins/VstEffect/VstEffectControls.cpp index cf0c831a6b8..af90e46464e 100644 --- a/plugins/VstEffect/VstEffectControls.cpp +++ b/plugins/VstEffect/VstEffectControls.cpp @@ -50,7 +50,6 @@ VstEffectControls::VstEffectControls( VstEffect * _eff ) : EffectControls( _eff ), m_effect( _eff ), m_subWindow( nullptr ), - knobFModel( nullptr ), ctrHandle( nullptr ), lastPosInMenu (0), m_vstGuiVisible ( true ) @@ -84,7 +83,7 @@ void VstEffectControls::loadSettings( const QDomElement & _this ) const QMap & dump = m_effect->m_plugin->parameterDump(); paramCount = dump.size(); auto paramStr = std::array{}; - knobFModel = new FloatModel *[ paramCount ]; + knobFModel.resize(paramCount); QStringList s_dumpValues; for( int i = 0; i < paramCount; i++ ) { @@ -131,7 +130,7 @@ void VstEffectControls::saveSettings( QDomDocument & _doc, QDomElement & _this ) if( m_effect->m_plugin != nullptr ) { m_effect->m_plugin->saveSettings( _doc, _this ); - if (knobFModel != nullptr) { + if (!knobFModel.empty()) { const QMap & dump = m_effect->m_plugin->parameterDump(); paramCount = dump.size(); auto paramStr = std::array{}; @@ -376,8 +375,9 @@ ManageVSTEffectView::ManageVSTEffectView( VstEffect * _eff, VstEffectControls * vstKnobs = new CustomTextKnob *[ m_vi->paramCount ]; bool hasKnobModel = true; - if (m_vi->knobFModel == nullptr) { - m_vi->knobFModel = new FloatModel *[ m_vi->paramCount ]; + if (m_vi->knobFModel.empty()) + { + m_vi->knobFModel.resize(m_vi->paramCount); hasKnobModel = false; } @@ -543,7 +543,7 @@ void ManageVSTEffectView::syncParameterText() ManageVSTEffectView::~ManageVSTEffectView() { - if( m_vi2->knobFModel != nullptr ) + if (!m_vi2->knobFModel.empty()) { for( int i = 0; i < m_vi2->paramCount; i++ ) { @@ -558,11 +558,7 @@ ManageVSTEffectView::~ManageVSTEffectView() vstKnobs = nullptr; } - if( m_vi2->knobFModel != nullptr ) - { - delete [] m_vi2->knobFModel; - m_vi2->knobFModel = nullptr; - } + m_vi2->knobFModel.clear(); if( m_vi2->m_scrollArea != nullptr ) { diff --git a/plugins/VstEffect/VstEffectControls.h b/plugins/VstEffect/VstEffectControls.h index 42178b5b645..e2bea36e88c 100644 --- a/plugins/VstEffect/VstEffectControls.h +++ b/plugins/VstEffect/VstEffectControls.h @@ -89,7 +89,7 @@ protected slots: QMdiSubWindow * m_subWindow; QScrollArea * m_scrollArea; - FloatModel ** knobFModel; + std::vector knobFModel; int paramCount; QObject * ctrHandle; diff --git a/plugins/Watsyn/CMakeLists.txt b/plugins/Watsyn/CMakeLists.txt index 5aec12a46d2..b43abbb07b7 100644 --- a/plugins/Watsyn/CMakeLists.txt +++ b/plugins/Watsyn/CMakeLists.txt @@ -1,5 +1,8 @@ -INCLUDE(BuildPlugin) +include(BuildPlugin) -LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) -LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) -BUILD_PLUGIN(watsyn Watsyn.cpp Watsyn.h MOCFILES Watsyn.h EMBEDDED_RESOURCES *.png) +build_plugin(watsyn + Watsyn.cpp Watsyn.h + MOCFILES Watsyn.h + EMBEDDED_RESOURCES *.png +) +target_link_libraries(watsyn SampleRate::samplerate) diff --git a/plugins/ZynAddSubFx/zynaddsubfx b/plugins/ZynAddSubFx/zynaddsubfx index 551e816a633..7ad5663cbee 160000 --- a/plugins/ZynAddSubFx/zynaddsubfx +++ b/plugins/ZynAddSubFx/zynaddsubfx @@ -1 +1 @@ -Subproject commit 551e816a6334fd190c74ce971378063b2757b47b +Subproject commit 7ad5663cbeebc02d73fd3ad666e428c1287f2cda diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f483d8b4137..c074ea2ef5e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,8 +58,6 @@ FILE(RELATIVE_PATH PLUGIN_DIR_RELATIVE "/${BIN_DIR}" "/${PLUGIN_DIR}") ADD_DEFINITIONS(-DLIB_DIR="${LIB_DIR_RELATIVE}" -DPLUGIN_DIR="${PLUGIN_DIR_RELATIVE}" ${PULSEAUDIO_DEFINITIONS}) INCLUDE_DIRECTORIES( ${JACK_INCLUDE_DIRS} - ${SAMPLERATE_INCLUDE_DIRS} - ${SNDFILE_INCLUDE_DIRS} ${SNDIO_INCLUDE_DIRS} ${FFTW3F_INCLUDE_DIRS} ) @@ -79,10 +77,6 @@ IF(NOT ("${PULSEAUDIO_INCLUDE_DIR}" STREQUAL "")) INCLUDE_DIRECTORIES("${PULSEAUDIO_INCLUDE_DIR}") ENDIF() -IF(NOT ("${OGGVORBIS_INCLUDE_DIR}" STREQUAL "")) - INCLUDE_DIRECTORIES("${OGGVORBIS_INCLUDE_DIR}") -ENDIF() - IF(NOT ("${LV2_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES(${LV2_INCLUDE_DIRS}) ENDIF() @@ -170,6 +164,10 @@ if(LMMS_HAVE_MP3LAME) list(APPEND EXTRA_LIBRARIES mp3lame::mp3lame) endif() +if(LMMS_HAVE_OGGVORBIS) + list(APPEND EXTRA_LIBRARIES Vorbis::vorbisenc Vorbis::vorbisfile) +endif() + if(LMMS_USE_MINGW_STD_THREADS) list(APPEND EXTRA_LIBRARIES mingw_stdthreads) endif() @@ -184,15 +182,14 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${SNDIO_LIBRARIES} ${PULSEAUDIO_LIBRARIES} ${JACK_LIBRARIES} - ${OGGVORBIS_LIBRARIES} ${LV2_LIBRARIES} ${SUIL_LIBRARIES} ${LILV_LIBRARIES} - ${SAMPLERATE_LIBRARIES} - ${SNDFILE_LIBRARIES} ${FFTW3F_LIBRARIES} - ${EXTRA_LIBRARIES} rpmalloc + SampleRate::samplerate + SndFile::sndfile + ${EXTRA_LIBRARIES} ) # Expose required libs for tests binary @@ -211,10 +208,11 @@ FOREACH(LIB ${LMMS_REQUIRED_LIBS}) ENDIF() ENDFOREACH() +set_target_properties(lmms PROPERTIES + ENABLE_EXPORTS ON +) + IF(LMMS_BUILD_WIN32) - SET_TARGET_PROPERTIES(lmms PROPERTIES - ENABLE_EXPORTS ON - ) IF(NOT MSVC) SET_PROPERTY(TARGET lmms APPEND_STRING PROPERTY LINK_FLAGS " -mwindows" @@ -228,10 +226,6 @@ IF(LMMS_BUILD_WIN32) ) ENDIF() ELSE() - IF(NOT LMMS_BUILD_APPLE) - SET_TARGET_PROPERTIES(lmms PROPERTIES LINK_FLAGS "${LINK_FLAGS} -Wl,-E") - ENDIF(NOT LMMS_BUILD_APPLE) - if(CMAKE_INSTALL_MANDIR) SET(INSTALL_MANDIR ${CMAKE_INSTALL_MANDIR}) ELSE(CMAKE_INSTALL_MANDIR) diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 29c54647cf7..47b42e11b9a 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -67,6 +67,7 @@ namespace lmms using LocklessListElement = LocklessList::Element; static thread_local bool s_renderingThread; +static thread_local bool s_runningChange; @@ -83,19 +84,12 @@ AudioEngine::AudioEngine( bool renderOnly ) : m_newPlayHandles( PlayHandle::MaxNumber ), m_qualitySettings( qualitySettings::Mode::Draft ), m_masterGain( 1.0f ), - m_isProcessing( false ), m_audioDev( nullptr ), m_oldAudioDev( nullptr ), m_audioDevStartFailed( false ), m_profiler(), m_metronomeActive(false), - m_clearSignal( false ), - m_changesSignal( false ), - m_changes( 0 ), -#if (QT_VERSION < QT_VERSION_CHECK(5,14,0)) - m_doChangesMutex( QMutex::Recursive ), -#endif - m_waitingForWrite( false ) + m_clearSignal(false) { for( int i = 0; i < 2; ++i ) { @@ -126,6 +120,9 @@ AudioEngine::AudioEngine( bool renderOnly ) : m_framesPerPeriod = DEFAULT_BUFFER_SIZE; } + // lmms works with chunks of size DEFAULT_BUFFER_SIZE (256) and only the final mix will use the actual + // buffer size. Plugins don't see a larger buffer size than 256. If m_framesPerPeriod is larger than + // DEFAULT_BUFFER_SIZE, it's set to DEFAULT_BUFFER_SIZE and the rest is handled by an increased fifoSize. else if( m_framesPerPeriod > DEFAULT_BUFFER_SIZE ) { fifoSize = m_framesPerPeriod / DEFAULT_BUFFER_SIZE; @@ -162,8 +159,6 @@ AudioEngine::AudioEngine( bool renderOnly ) : AudioEngine::~AudioEngine() { - runChangesInModel(); - for( int w = 0; w < m_numWorkers; ++w ) { m_workers[w]->quit(); @@ -229,8 +224,6 @@ void AudioEngine::startProcessing(bool needsFifo) } m_audioDev->startProcessing(); - - m_isProcessing = true; } @@ -238,8 +231,6 @@ void AudioEngine::startProcessing(bool needsFifo) void AudioEngine::stopProcessing() { - m_isProcessing = false; - if( m_fifoWriter != nullptr ) { m_fifoWriter->finish(); @@ -394,6 +385,17 @@ void AudioEngine::renderStageInstruments() AudioEngineWorkerThread::fillJobQueue(m_playHandles); AudioEngineWorkerThread::startAndWaitForJobs(); +} + + + +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(); // removed all play handles which are done for( PlayHandleList::Iterator it = m_playHandles.begin(); @@ -424,17 +426,6 @@ void AudioEngine::renderStageInstruments() -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(); -} - - - void AudioEngine::renderStageMix() { AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Mixing); @@ -444,8 +435,6 @@ void AudioEngine::renderStageMix() emit nextAudioBuffer(m_outputBufferRead); - runChangesInModel(); - // and trigger LFOs EnvelopeAndLfoParameters::instances()->trigger(); Controller::triggerFrameCounter(); @@ -456,6 +445,8 @@ void AudioEngine::renderStageMix() const surroundSampleFrame *AudioEngine::renderNextBuffer() { + const auto lock = std::lock_guard{m_changeMutex}; + m_profiler.startPeriod(); s_renderingThread = true; @@ -808,57 +799,16 @@ void AudioEngine::removePlayHandlesOfTypes(Track * track, PlayHandle::Types type void AudioEngine::requestChangeInModel() { - if( s_renderingThread ) - return; - - m_changesMutex.lock(); - m_changes++; - m_changesMutex.unlock(); - - m_doChangesMutex.lock(); - m_waitChangesMutex.lock(); - if (m_isProcessing && !m_waitingForWrite && !m_changesSignal) - { - m_changesSignal = true; - m_changesRequestCondition.wait( &m_waitChangesMutex ); - } - m_waitChangesMutex.unlock(); + if (s_renderingThread || s_runningChange) { return; } + m_changeMutex.lock(); + s_runningChange = true; } - - - void AudioEngine::doneChangeInModel() { - if( s_renderingThread ) - return; - - m_changesMutex.lock(); - bool moreChanges = --m_changes; - m_changesMutex.unlock(); - - if( !moreChanges ) - { - m_changesSignal = false; - m_changesAudioEngineCondition.wakeOne(); - } - m_doChangesMutex.unlock(); -} - - - - -void AudioEngine::runChangesInModel() -{ - if( m_changesSignal ) - { - m_waitChangesMutex.lock(); - // allow changes in the model from other threads ... - m_changesRequestCondition.wakeOne(); - // ... and wait until they are done - m_changesAudioEngineCondition.wait( &m_waitChangesMutex ); - m_waitChangesMutex.unlock(); - } + if (s_renderingThread || !s_runningChange) { return; } + m_changeMutex.unlock(); + s_runningChange = false; } bool AudioEngine::isAudioDevNameValid(QString name) @@ -1294,29 +1244,12 @@ void AudioEngine::fifoWriter::run() auto buffer = new surroundSampleFrame[frames]; const surroundSampleFrame * b = m_audioEngine->renderNextBuffer(); memcpy( buffer, b, frames * sizeof( surroundSampleFrame ) ); - write( buffer ); + m_fifo->write(buffer); } // Let audio backend stop processing - write( nullptr ); + m_fifo->write(nullptr); m_fifo->waitUntilRead(); } - - - -void AudioEngine::fifoWriter::write( surroundSampleFrame * buffer ) -{ - m_audioEngine->m_waitChangesMutex.lock(); - m_audioEngine->m_waitingForWrite = true; - m_audioEngine->m_waitChangesMutex.unlock(); - m_audioEngine->runChangesInModel(); - - m_fifo->write( buffer ); - - m_audioEngine->m_doChangesMutex.lock(); - m_audioEngine->m_waitingForWrite = false; - m_audioEngine->m_doChangesMutex.unlock(); -} - } // namespace lmms diff --git a/src/core/AutomationClip.cpp b/src/core/AutomationClip.cpp index 3b36f6b49b7..6035502704d 100644 --- a/src/core/AutomationClip.cpp +++ b/src/core/AutomationClip.cpp @@ -422,6 +422,32 @@ void AutomationClip::resetNodes(const int tick0, const int tick1) +void AutomationClip::resetTangents(const int tick0, const int tick1) +{ + if (tick0 == tick1) + { + auto it = m_timeMap.find(TimePos(tick0)); + if (it != m_timeMap.end()) + { + it.value().setLockedTangents(false); + generateTangents(it, 1); + } + return; + } + + TimePos start = TimePos(std::min(tick0, tick1)); + TimePos end = TimePos(std::max(tick0, tick1)); + + for (auto it = m_timeMap.lowerBound(start), endIt = m_timeMap.upperBound(end); it != endIt; ++it) + { + it.value().setLockedTangents(false); + generateTangents(it, 1); + } +} + + + + void AutomationClip::recordValue(TimePos time, float value) { QMutexLocker m(&m_clipMutex); @@ -467,16 +493,31 @@ TimePos AutomationClip::setDragValue( // inValue m_dragKeepOutValue = false; + // We will set the tangents back to what they were if the node had + // its tangents locked + m_dragLockedTan = false; + // Check if we already have a node on the position we are dragging // and if we do, store the outValue so the discrete jump can be kept + // and information about the tangents timeMap::iterator it = m_timeMap.find(newTime); if (it != m_timeMap.end()) { + // If we don't have a discrete jump, the outValue will be the + // same as the inValue if (OFFSET(it) != 0) { m_dragKeepOutValue = true; m_dragOutValue = OUTVAL(it); } + // For the tangents, we will only keep them if the tangents were + // locked + if (LOCKEDTAN(it)) + { + m_dragLockedTan = true; + m_dragInTan = INTAN(it); + m_dragOutTan = OUTTAN(it); + } } this->removeNode(newTime); @@ -489,12 +530,31 @@ TimePos AutomationClip::setDragValue( generateTangents(); + TimePos returnedPos; + if (m_dragKeepOutValue) { - return this->putValues(time, value, m_dragOutValue, quantPos, controlKey); + returnedPos = this->putValues(time, value, m_dragOutValue, quantPos, controlKey); + } + else + { + returnedPos = this->putValue(time, value, quantPos, controlKey); + } + + // Set the tangents on the newly created node if they were locked + // before dragging + if (m_dragLockedTan) + { + timeMap::iterator it = m_timeMap.find(returnedPos); + if (it != m_timeMap.end()) + { + it.value().setInTangent(m_dragInTan); + it.value().setOutTangent(m_dragOutTan); + it.value().setLockedTangents(true); + } } - return this->putValue(time, value, quantPos, controlKey); + return returnedPos; } @@ -770,10 +830,10 @@ void AutomationClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "prog", QString::number( static_cast(progressionType()) ) ); _this.setAttribute( "tens", QString::number( getTension() ) ); _this.setAttribute( "mute", QString::number( isMuted() ) ); - - if( usesCustomClipColor() ) + + if (const auto& c = color()) { - _this.setAttribute( "color", color().name() ); + _this.setAttribute("color", c->name()); } for( timeMap::const_iterator it = m_timeMap.begin(); @@ -783,6 +843,9 @@ void AutomationClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) element.setAttribute("pos", POS(it)); element.setAttribute("value", INVAL(it)); element.setAttribute("outValue", OUTVAL(it)); + element.setAttribute("inTan", INTAN(it)); + element.setAttribute("outTan", OUTTAN(it)); + element.setAttribute("lockedTan", static_cast(LOCKEDTAN(it))); _this.appendChild( element ); } @@ -804,6 +867,11 @@ void AutomationClip::loadSettings( const QDomElement & _this ) { QMutexLocker m(&m_clipMutex); + // Legacy compatibility: Previously tangents were not stored in + // the project file. So if any node doesn't have tangent information + // we will generate the tangents + bool shouldGenerateTangents = false; + clear(); movePosition( _this.attribute( "pos" ).toInt() ); @@ -828,6 +896,22 @@ void AutomationClip::loadSettings( const QDomElement & _this ) float timeMapOutValue = LocaleHelper::toFloat(element.attribute("outValue")); m_timeMap[timeMapPos] = AutomationNode(this, timeMapInValue, timeMapOutValue, timeMapPos); + + // Load tangents if there is information about it (it's enough to check for either inTan or outTan) + if (element.hasAttribute("inTan")) + { + float inTan = LocaleHelper::toFloat(element.attribute("inTan")); + float outTan = LocaleHelper::toFloat(element.attribute("outTan")); + bool lockedTan = static_cast(element.attribute("lockedTan", "0").toInt()); + + m_timeMap[timeMapPos].setInTangent(inTan); + m_timeMap[timeMapPos].setOutTangent(outTan); + m_timeMap[timeMapPos].setLockedTangents(lockedTan); + } + else + { + shouldGenerateTangents = true; + } } else if( element.tagName() == "object" ) { @@ -835,10 +919,9 @@ void AutomationClip::loadSettings( const QDomElement & _this ) } } - if( _this.hasAttribute( "color" ) ) + if (_this.hasAttribute("color")) { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); + setColor(QColor{_this.attribute("color")}); } int len = _this.attribute( "len" ).toInt(); @@ -851,7 +934,8 @@ void AutomationClip::loadSettings( const QDomElement & _this ) { changeLength( len ); } - generateTangents(); + + if (shouldGenerateTangents) { generateTangents(); } } @@ -1108,6 +1192,12 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) for (int i = 0; i < numToGenerate && it != m_timeMap.end(); ++i, ++it) { + // Skip the node if it has locked tangents (were manually edited) + if (LOCKEDTAN(it)) + { + continue; + } + if (it + 1 == m_timeMap.end()) { // Previously, the last value's tangent was always set to 0. That logic was kept for both tangents diff --git a/src/core/AutomationNode.cpp b/src/core/AutomationNode.cpp index eee4df8d21e..f15c28f8028 100644 --- a/src/core/AutomationNode.cpp +++ b/src/core/AutomationNode.cpp @@ -37,7 +37,8 @@ AutomationNode::AutomationNode() : m_inValue(0), m_outValue(0), m_inTangent(0), - m_outTangent(0) + m_outTangent(0), + m_lockedTangents(false) { } @@ -47,7 +48,8 @@ AutomationNode::AutomationNode(AutomationClip* clip, float value, int pos) : m_inValue(value), m_outValue(value), m_inTangent(0), - m_outTangent(0) + m_outTangent(0), + m_lockedTangents(false) { } @@ -57,7 +59,8 @@ AutomationNode::AutomationNode(AutomationClip* clip, float inValue, float outVal m_inValue(inValue), m_outValue(outValue), m_inTangent(0), - m_outTangent(0) + m_outTangent(0), + m_lockedTangents(false) { } diff --git a/src/core/Clip.cpp b/src/core/Clip.cpp index db1200aae2d..b18391df169 100644 --- a/src/core/Clip.cpp +++ b/src/core/Clip.cpp @@ -48,9 +48,7 @@ Clip::Clip( Track * track ) : m_startPosition(), m_length(), m_mutedModel( false, this, tr( "Mute" ) ), - m_selectViewOnCreate( false ), - m_color( 128, 128, 128 ), - m_useCustomClipColor( false ) + m_selectViewOnCreate{false} { if( getTrack() ) { @@ -185,19 +183,10 @@ void Clip::setStartTimeOffset( const TimePos &startTimeOffset ) m_startTimeOffset = startTimeOffset; } - - -void Clip::useCustomClipColor( bool b ) +void Clip::setColor(const std::optional& color) { - if (b == m_useCustomClipColor) { return; } - m_useCustomClipColor = b; + m_color = color; emit colorChanged(); } - -bool Clip::hasColor() -{ - return usesCustomClipColor() || getTrack()->useColor(); -} - } // namespace lmms diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 8d0a8dca43f..a520e6bc5c9 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include "base64.h" #include "ConfigManager.h" @@ -42,6 +43,7 @@ #include "embed.h" #include "GuiApplication.h" #include "LocaleHelper.h" +#include "Note.h" #include "PluginFactory.h" #include "ProjectVersion.h" #include "SongEditor.h" @@ -79,7 +81,8 @@ 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 + &DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing, + &DataFile::upgrade_loopsRename , &DataFile::upgrade_noteTypes }; // Vector of all versions that have upgrade routines. @@ -231,8 +234,11 @@ bool DataFile::validate( QString extension ) { return true; } - if( extension == "wav" || extension == "ogg" || - extension == "ds" ) + if( extension == "wav" || extension == "ogg" || extension == "ds" +#ifdef LMMS_HAVE_SNDFILE_MP3 + || extension == "mp3" +#endif + ) { return true; } @@ -302,7 +308,7 @@ void DataFile::write( QTextStream & _strm ) bool DataFile::writeFile(const QString& filename, bool withResources) { // Small lambda function for displaying errors - auto showError = [this](QString title, QString body){ + auto showError = [](QString title, QString body){ if (gui::getGUI() != nullptr) { QMessageBox mb; @@ -376,12 +382,12 @@ bool DataFile::writeFile(const QString& filename, bool withResources) } } - QFile outfile (fullNameTemp); + QSaveFile outfile(fullNameTemp); if (!outfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { showError(SongEditor::tr("Could not write file"), - SongEditor::tr("Could not open %1 for writing. You probably are not permitted to" + SongEditor::tr("Could not open %1 for writing. You probably are not permitted to " "write to this file. Please make sure you have write-access to " "the file and try again.").arg(fullName)); @@ -402,30 +408,29 @@ bool DataFile::writeFile(const QString& filename, bool withResources) write( ts ); } - outfile.close(); - - // make sure the file has been written correctly - if( QFileInfo( outfile.fileName() ).size() > 0 ) + if (!outfile.commit()) { - if( ConfigManager::inst()->value( "app", "disablebackup" ).toInt() ) - { - // remove current file - QFile::remove( fullName ); - } - else - { - // remove old backup file - QFile::remove( fullNameBak ); - // move current file to backup file - QFile::rename( fullName, fullNameBak ); - } - // move temporary file to current file - QFile::rename( fullNameTemp, fullName ); + showError(SongEditor::tr("Could not write file"), + SongEditor::tr("An unknown error has occured and the file could not be saved.")); + return false; + } - return true; + if (ConfigManager::inst()->value("app", "disablebackup").toInt()) + { + // remove current file + QFile::remove(fullName); } + else + { + // remove old backup file + QFile::remove(fullNameBak); + // move current file to backup file + QFile::rename(fullName, fullNameBak); + } + // move temporary file to current file + QFile::rename(fullNameTemp, fullName); - return false; + return true; } @@ -1662,6 +1667,24 @@ void DataFile::upgrade_automationNodes() } } +// Convert the negative length notes to StepNotes +void DataFile::upgrade_noteTypes() +{ + const auto notes = elementsByTagName("note"); + + for (int i = 0; i < notes.size(); ++i) + { + auto note = notes.item(i).toElement(); + + const auto noteSize = note.attribute("len").toInt(); + if (noteSize < 0) + { + note.setAttribute("len", DefaultTicksPerBar / 16); + note.setAttribute("type", static_cast(Note::Type::Step)); + } + } +} + /** \brief Note range has been extended to match MIDI specification * @@ -1804,7 +1827,76 @@ void DataFile::upgrade_sampleAndHold() // Correct old random wave LFO speeds if (e.attribute("wave").toInt() == 6) { - e.setAttribute("speed",0.01f); + e.setAttribute("speed", 0.01f); + } + } +} + + +// Change loops' filenames in s +void DataFile::upgrade_loopsRename() +{ + static constexpr auto loopBPMs = std::array{ + std::pair{"bassloops/briff01", "140"}, + std::pair{"bassloops/rave_bass01", "180"}, + std::pair{"bassloops/rave_bass02", "180"}, + std::pair{"bassloops/tb303_01", "123"}, + std::pair{"bassloops/techno_bass01", "140"}, + std::pair{"bassloops/techno_bass02", "140"}, + std::pair{"bassloops/techno_synth01", "140"}, + std::pair{"bassloops/techno_synth02", "140"}, + std::pair{"bassloops/techno_synth03", "130"}, + std::pair{"bassloops/techno_synth04", "140"}, + std::pair{"beats/909beat01", "122"}, + std::pair{"beats/break01", "168"}, + std::pair{"beats/break02", "141"}, + std::pair{"beats/break03", "168"}, + std::pair{"beats/electro_beat01", "120"}, + std::pair{"beats/electro_beat02", "119"}, + std::pair{"beats/house_loop01", "142"}, + std::pair{"beats/jungle01", "168"}, + std::pair{"beats/rave_hihat01", "180"}, + std::pair{"beats/rave_hihat02", "180"}, + std::pair{"beats/rave_kick01", "180"}, + std::pair{"beats/rave_kick02", "180"}, + std::pair{"beats/rave_snare01", "180"}, + std::pair{"latin/latin_brass01", "140"}, + std::pair{"latin/latin_guitar01", "126"}, + std::pair{"latin/latin_guitar02", "140"}, + std::pair{"latin/latin_guitar03", "120"}, + }; + + const QString prefix = "factorysample:", + extension = ".ogg"; + + // Replace loop sample names + for (const auto& [elem, srcAttrs] : ELEMENTS_WITH_RESOURCES) + { + auto elements = elementsByTagName(elem); + + for (const auto& srcAttr : srcAttrs) + { + for (int i = 0; i < elements.length(); ++i) + { + auto item = elements.item(i).toElement(); + + if (item.isNull() || !item.hasAttribute(srcAttr)) { continue; } + for (const auto& cur : loopBPMs) + { + QString x = cur.first, // loop name + y = cur.second, // BPM + srcVal = item.attribute(srcAttr), + pattern = prefix + x + extension; + + if (srcVal == pattern) + { + // Add " - X BPM" to filename + item.setAttribute(srcAttr, + prefix + x + " - " + y + " BPM" + + extension); + } + } + } } } } @@ -1981,5 +2073,4 @@ unsigned int DataFile::legacyFileVersion() return std::distance( UPGRADE_VERSIONS.begin(), firstRequiredUpgrade ); } - } // namespace lmms diff --git a/src/core/DrumSynth.cpp b/src/core/DrumSynth.cpp index bc5455c96fc..decc6bfa26d 100644 --- a/src/core/DrumSynth.cpp +++ b/src/core/DrumSynth.cpp @@ -273,7 +273,9 @@ int DrumSynth::GetDSFileSamples(QString dsfile, int16_t *&wave, int channels, sa //generation long Length, tpos=0, tplus, totmp, t, i, j; float x[3] = {0.f, 0.f, 0.f}; - float MasterTune, randmax, randmax2; + float MasterTune; + constexpr float randmax = 1.f / static_cast(RAND_MAX); + constexpr float randmax2 = 2.f / static_cast(RAND_MAX); int MainFilter, HighPass; long NON, NT, TON, DiON, TDroop=0, DStep; @@ -454,7 +456,6 @@ int DrumSynth::GetDSFileSamples(QString dsfile, int16_t *&wave, int channels, sa } //prepare envelopes - randmax = 1.f / RAND_MAX; randmax2 = 2.f * randmax; for (i=1;i<8;i++) { envData[i][NEXTT]=0; envData[i][PNT]=0; } Length = LongestEnv(); @@ -745,4 +746,4 @@ int DrumSynth::GetDSFileSamples(QString dsfile, int16_t *&wave, int channels, sa } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index b715bcac02c..a7cfc467ba4 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -179,21 +179,18 @@ void Instrument::applyFadeIn(sampleFrame * buf, NotePlayHandle * n) void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n ) { - const fpp_t frames = _n->framesLeftForCurrentPeriod(); - const fpp_t fpp = Engine::audioEngine()->framesPerPeriod(); - const f_cnt_t fl = _n->framesLeft(); - if( fl <= desiredReleaseFrames()+fpp ) + const auto fpp = Engine::audioEngine()->framesPerPeriod(); + const auto releaseFrames = desiredReleaseFrames(); + + const auto endFrame = _n->framesLeft(); + const auto startFrame = std::max(0, endFrame - releaseFrames); + + for (auto f = startFrame; f < endFrame && f < fpp; f++) { - for( fpp_t f = (fpp_t)( ( fl > desiredReleaseFrames() ) ? - (std::max(fpp - desiredReleaseFrames(), 0) + - fl % fpp) : 0); f < frames; ++f) + const float fac = (float)(endFrame - f) / (float)releaseFrames; + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ch++) { - const float fac = (float)( fl-f-1 ) / - desiredReleaseFrames(); - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) - { - buf[f][ch] *= fac; - } + buf[f][ch] *= fac; } } } diff --git a/src/core/InstrumentFunctions.cpp b/src/core/InstrumentFunctions.cpp index 431afd2fe5c..976363d3d08 100644 --- a/src/core/InstrumentFunctions.cpp +++ b/src/core/InstrumentFunctions.cpp @@ -409,7 +409,7 @@ void InstrumentFunctionArpeggio::processNote( NotePlayHandle * _n ) // Skip notes randomly if( m_arpSkipModel.value() ) { - if( 100 * ( (float) rand() / (float)( RAND_MAX + 1.0f ) ) < m_arpSkipModel.value() ) + if (100 * static_cast(rand()) / (static_cast(RAND_MAX) + 1.0f) < m_arpSkipModel.value()) { // update counters frames_processed += arp_frames; @@ -425,7 +425,7 @@ void InstrumentFunctionArpeggio::processNote( NotePlayHandle * _n ) if( m_arpMissModel.value() ) { - if( 100 * ( (float) rand() / (float)( RAND_MAX + 1.0f ) ) < m_arpMissModel.value() ) + if (100 * static_cast(rand()) / (static_cast(RAND_MAX) + 1.0f) < m_arpMissModel.value()) { dir = ArpDirection::Random; } diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index 59c2dd72e16..6dd2e34510b 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -72,7 +72,6 @@ MixerChannel::MixerChannel( int idx, Model * _parent ) : m_lock(), m_channelIndex( idx ), m_queued( false ), - m_hasColor( false ), m_dependenciesMet(0) { BufferManager::clear( m_buffer, Engine::audioEngine()->framesPerPeriod() ); @@ -722,6 +721,7 @@ void Mixer::clearChannel(mix_ch_t index) ch->m_volumeModel.setDisplayName( ch->m_name + ">" + tr( "Volume" ) ); ch->m_muteModel.setDisplayName( ch->m_name + ">" + tr( "Mute" ) ); ch->m_soloModel.setDisplayName( ch->m_name + ">" + tr( "Solo" ) ); + ch->setColor(std::nullopt); // send only to master if( index > 0) @@ -759,7 +759,7 @@ void Mixer::saveSettings( QDomDocument & _doc, QDomElement & _this ) ch->m_soloModel.saveSettings( _doc, mixch, "soloed" ); mixch.setAttribute( "num", i ); mixch.setAttribute( "name", ch->m_name ); - if( ch->m_hasColor ) mixch.setAttribute( "color", ch->m_color.name() ); + if (const auto& color = ch->color()) { mixch.setAttribute("color", color->name()); } // add the channel sends for (const auto& send : ch->m_sends) @@ -805,10 +805,9 @@ void Mixer::loadSettings( const QDomElement & _this ) m_mixerChannels[num]->m_muteModel.loadSettings( mixch, "muted" ); m_mixerChannels[num]->m_soloModel.loadSettings( mixch, "soloed" ); m_mixerChannels[num]->m_name = mixch.attribute( "name" ); - if( mixch.hasAttribute( "color" ) ) + if (mixch.hasAttribute("color")) { - m_mixerChannels[num]->m_hasColor = true; - m_mixerChannels[num]->m_color.setNamedColor( mixch.attribute( "color" ) ); + m_mixerChannels[num]->setColor(QColor{mixch.attribute("color")}); } m_mixerChannels[num]->m_fxChain.restoreState( mixch.firstChildElement( diff --git a/src/core/Note.cpp b/src/core/Note.cpp index a4ad61412f4..ed3a00f1017 100644 --- a/src/core/Note.cpp +++ b/src/core/Note.cpp @@ -74,7 +74,8 @@ Note::Note( const Note & note ) : m_panning( note.m_panning ), m_length( note.m_length ), m_pos( note.m_pos ), - m_detuning( nullptr ) + m_detuning(nullptr), + m_type(note.m_type) { if( note.m_detuning ) { @@ -179,6 +180,7 @@ void Note::saveSettings( QDomDocument & doc, QDomElement & parent ) parent.setAttribute( "pan", m_panning ); parent.setAttribute( "len", m_length ); parent.setAttribute( "pos", m_pos ); + parent.setAttribute("type", static_cast(m_type)); if( m_detuning && m_length ) { @@ -197,6 +199,9 @@ void Note::loadSettings( const QDomElement & _this ) m_panning = _this.attribute( "pan" ).toInt(); m_length = _this.attribute( "len" ).toInt(); m_pos = _this.attribute( "pos" ).toInt(); + // Default m_type value is 0, which corresponds to RegularNote + static_assert(0 == static_cast(Type::Regular)); + m_type = static_cast(_this.attribute("type", "0").toInt()); if( _this.hasChildNodes() ) { diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index eb9c7ddbff4..712b64e8917 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -53,7 +53,7 @@ NotePlayHandle::NotePlayHandle( InstrumentTrack* instrumentTrack, NotePlayHandle *parent, int midiEventChannel, Origin origin ) : - PlayHandle( Type::NotePlayHandle, _offset ), + PlayHandle( PlayHandle::Type::NotePlayHandle, _offset ), Note( n.length(), n.pos(), n.key(), n.getVolume(), n.getPanning(), n.detuning() ), m_pluginData( nullptr ), m_instrumentTrack( instrumentTrack ), diff --git a/src/core/PatternClip.cpp b/src/core/PatternClip.cpp index 1058da6ba38..15a1d1f543d 100644 --- a/src/core/PatternClip.cpp +++ b/src/core/PatternClip.cpp @@ -62,9 +62,9 @@ void PatternClip::saveSettings(QDomDocument& doc, QDomElement& element) element.setAttribute( "len", length() ); element.setAttribute("off", startTimeOffset()); element.setAttribute( "muted", isMuted() ); - if( usesCustomClipColor() ) + if (const auto& c = color()) { - element.setAttribute( "color", color().name() ); + element.setAttribute("color", c->name()); } } @@ -90,20 +90,14 @@ void PatternClip::loadSettings(const QDomElement& element) if (!element.hasAttribute("usestyle")) { // for colors saved in 1.3-onwards - setColor(element.attribute("color")); - useCustomClipColor(true); + setColor(QColor{element.attribute("color")}); } - else + else if (element.attribute("usestyle").toUInt() == 0) { // for colors saved before 1.3 - setColor(QColor(element.attribute("color").toUInt())); - useCustomClipColor(element.attribute("usestyle").toUInt() == 0); + setColor(QColor{element.attribute("color").toUInt()}); } } - else - { - useCustomClipColor(false); - } } diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index c96111bba45..2a0076a283c 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -305,10 +305,6 @@ void SampleBuffer::update(bool keepSettings) } sf_close(sndFile); } - else - { - fileLoadError = FileLoadError::Invalid; - } f.close(); } @@ -337,6 +333,11 @@ void SampleBuffer::update(bool keepSettings) { m_frames = decodeSampleDS(file, buf, channels, samplerate); } + + if (m_frames == 0) + { + fileLoadError = FileLoadError::Invalid; + } } if (m_frames == 0 || fileLoadError != FileLoadError::None) // if still no frames, bail @@ -1184,14 +1185,20 @@ QString SampleBuffer::openAudioFile() const // set filters QStringList types; - types << tr("All Audio-Files (*.wav *.ogg *.ds *.flac *.spx *.voc " + types << tr("All Audio-Files (*.wav *.ogg " +#ifdef LMMS_HAVE_SNDFILE_MP3 + "*.mp3 " +#endif + "*.ds *.flac *.spx *.voc " "*.aif *.aiff *.au *.raw)") << tr("Wave-Files (*.wav)") << tr("OGG-Files (*.ogg)") +#ifdef LMMS_HAVE_SNDFILE_MP3 + << tr("MP3-Files (*.mp3)") +#endif << tr("DrumSynth-Files (*.ds)") << tr("FLAC-Files (*.flac)") << tr("SPEEX-Files (*.spx)") - //<< tr("MP3-Files (*.mp3)") //<< tr("MIDI-Files (*.mid)") << tr("VOC-Files (*.voc)") << tr("AIFF-Files (*.aif *.aiff)") diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index b09d7b3bb2b..2febaee2e7f 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -271,9 +271,9 @@ void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) } _this.setAttribute( "sample_rate", m_sampleBuffer->sampleRate()); - if( usesCustomClipColor() ) + if (const auto& c = color()) { - _this.setAttribute( "color", color().name() ); + _this.setAttribute("color", c->name()); } if (m_sampleBuffer->reversed()) { @@ -304,14 +304,9 @@ void SampleClip::loadSettings( const QDomElement & _this ) setMuted( _this.attribute( "muted" ).toInt() ); setStartTimeOffset( _this.attribute( "off" ).toInt() ); - if( _this.hasAttribute( "color" ) ) + if (_this.hasAttribute("color")) { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); - } - else - { - useCustomClipColor(false); + setColor(QColor{_this.attribute("color")}); } if(_this.hasAttribute("reversed")) diff --git a/src/core/TimePos.cpp b/src/core/TimePos.cpp index 86a65f10316..09c1019bcef 100644 --- a/src/core/TimePos.cpp +++ b/src/core/TimePos.cpp @@ -25,6 +25,7 @@ #include "TimePos.h" +#include #include "MeterModel.h" namespace lmms @@ -161,11 +162,11 @@ tick_t TimePos::getTickWithinBeat( const TimeSig &sig ) const f_cnt_t TimePos::frames( const float framesPerTick ) const { - if( m_ticks >= 0 ) - { - return static_cast( m_ticks * framesPerTick ); - } - return 0; + // Before, step notes used to have negative length. This + // assert is a safeguard against negative length being + // introduced again (now using Note Types instead #5902) + assert(m_ticks >= 0); + return static_cast(m_ticks * framesPerTick); } double TimePos::getTimeInMilliseconds( bpm_t beatsPerMinute ) const @@ -221,4 +222,4 @@ double TimePos::ticksToMilliseconds(double ticks, bpm_t beatsPerMinute) } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 6628a2ad535..7a664a11e63 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -64,10 +64,8 @@ Track::Track( Type type, TrackContainer * tc ) : m_mutedModel( false, this, tr( "Mute" ) ), /*!< For controlling track muting */ m_soloModel( false, this, tr( "Solo" ) ), /*!< For controlling track soloing */ m_simpleSerializingMode( false ), - m_clips(), /*!< The clips (segments) */ - m_color( 0, 0, 0 ), - m_hasColor( false ) -{ + m_clips() /*!< The clips (segments) */ +{ m_trackContainer->addTrack( this ); m_height = -1; } @@ -209,9 +207,9 @@ void Track::saveSettings( QDomDocument & doc, QDomElement & element ) element.setAttribute( "trackheight", m_height ); } - if( m_hasColor ) + if (m_color.has_value()) { - element.setAttribute( "color", m_color.name() ); + element.setAttribute("color", m_color->name()); } QDomElement tsDe = doc.createElement( nodeName() ); @@ -264,14 +262,9 @@ void Track::loadSettings( const QDomElement & element ) // Older project files that didn't have this attribute will set the value to false (issue 5562) m_mutedBeforeSolo = QVariant( element.attribute( "mutedBeforeSolo", "0" ) ).toBool(); - if( element.hasAttribute( "color" ) ) - { - QColor newColor = QColor(element.attribute("color")); - setColor(newColor); - } - else + if (element.hasAttribute("color")) { - resetColor(); + setColor(QColor{element.attribute("color")}); } if( m_simpleSerializingMode ) @@ -634,20 +627,12 @@ void Track::toggleSolo() } } -void Track::setColor(const QColor& c) -{ - m_hasColor = true; - m_color = c; - emit colorChanged(); -} - -void Track::resetColor() +void Track::setColor(const std::optional& color) { - m_hasColor = false; + m_color = color; emit colorChanged(); } - BoolModel *Track::getMutedModel() { return &m_mutedModel; diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index 7371c7bfb93..a4fd2c095de 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -26,46 +26,47 @@ #ifdef LMMS_HAVE_JACK +#include #include -#include #include +#include "AudioEngine.h" +#include "ConfigManager.h" #include "Engine.h" #include "GuiApplication.h" -#include "gui_templates.h" -#include "ConfigManager.h" #include "LcdSpinBox.h" #include "MainWindow.h" -#include "AudioEngine.h" #include "MidiJack.h" - +#include "gui_templates.h" namespace lmms { -AudioJack::AudioJack( bool & _success_ful, AudioEngine* _audioEngine ) : - AudioDevice(std::clamp( - ConfigManager::inst()->value("audiojack", "channels").toInt(), - DEFAULT_CHANNELS, - SURROUND_CHANNELS), _audioEngine), - m_client( nullptr ), - m_active( false ), - m_midiClient( nullptr ), - m_tempOutBufs( new jack_default_audio_sample_t *[channels()] ), - m_outBuf( new surroundSampleFrame[audioEngine()->framesPerPeriod()] ), - m_framesDoneInCurBuf( 0 ), - m_framesToDoInCurBuf( 0 ) + +AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam) + : AudioDevice( + // clang-format off + std::clamp( + ConfigManager::inst()->value("audiojack", "channels").toInt(), + DEFAULT_CHANNELS, + SURROUND_CHANNELS + ), + // clang-format on + audioEngineParam) + , m_client(nullptr) + , m_active(false) + , m_midiClient(nullptr) + , m_tempOutBufs(new jack_default_audio_sample_t*[channels()]) + , m_outBuf(new surroundSampleFrame[audioEngine()->framesPerPeriod()]) + , m_framesDoneInCurBuf(0) + , m_framesToDoInCurBuf(0) { m_stopped = true; - _success_ful = initJackClient(); - if( _success_ful ) - { - connect( this, SIGNAL(zombified()), - this, SLOT(restartAfterZombified()), - Qt::QueuedConnection ); + successful = initJackClient(); + if (successful) { + connect(this, SIGNAL(zombified()), this, SLOT(restartAfterZombified()), Qt::QueuedConnection); } - } @@ -73,21 +74,18 @@ AudioJack::AudioJack( bool & _success_ful, AudioEngine* _audioEngine ) : AudioJack::~AudioJack() { - stopProcessing(); + AudioJack::stopProcessing(); #ifdef AUDIO_PORT_SUPPORT - while( m_portMap.size() ) + while (m_portMap.size()) { - unregisterPort( m_portMap.begin().key() ); + unregisterPort(m_portMap.begin().key()); } #endif - if( m_client != nullptr ) + if (m_client != nullptr) { - if( m_active ) - { - jack_deactivate( m_client ); - } - jack_client_close( m_client ); + if (m_active) { jack_deactivate(m_client); } + jack_client_close(m_client); } delete[] m_tempOutBufs; @@ -100,97 +98,79 @@ AudioJack::~AudioJack() void AudioJack::restartAfterZombified() { - if( initJackClient() ) + if (initJackClient()) { m_active = false; startProcessing(); - QMessageBox::information(gui::getGUI()->mainWindow(), - tr( "JACK client restarted" ), - tr( "LMMS was kicked by JACK for some reason. " + QMessageBox::information(gui::getGUI()->mainWindow(), tr("JACK client restarted"), + tr( "LMMS was kicked by JACK for some reason. " "Therefore the JACK backend of LMMS has been " "restarted. You will have to make manual " - "connections again." ) ); + "connections again.")); } else { - QMessageBox::information(gui::getGUI()->mainWindow(), - tr( "JACK server down" ), - tr( "The JACK server seems to have been shutdown " + QMessageBox::information(gui::getGUI()->mainWindow(), tr("JACK server down"), + tr( "The JACK server seems to have been shutdown " "and starting a new instance failed. " "Therefore LMMS is unable to proceed. " "You should save your project and restart " - "JACK and LMMS." ) ); + "JACK and LMMS.")); } } -AudioJack* AudioJack::addMidiClient(MidiJack *midiClient) + +AudioJack* AudioJack::addMidiClient(MidiJack* midiClient) { - if( m_client == nullptr ) - return nullptr; + if (m_client == nullptr) { return nullptr; } m_midiClient = midiClient; return this; } + + + bool AudioJack::initJackClient() { - QString clientName = ConfigManager::inst()->value( "audiojack", - "clientname" ); - if( clientName.isEmpty() ) - { - clientName = "lmms"; - } + QString clientName = ConfigManager::inst()->value("audiojack", "clientname"); + if (clientName.isEmpty()) { clientName = "lmms"; } - const char * serverName = nullptr; + const char* serverName = nullptr; jack_status_t status; - m_client = jack_client_open( clientName.toLatin1().constData(), - JackNullOption, &status, - serverName ); - if( m_client == nullptr ) + m_client = jack_client_open(clientName.toLatin1().constData(), JackNullOption, &status, serverName); + if (m_client == nullptr) { - printf( "jack_client_open() failed, status 0x%2.0x\n", status ); - if( status & JackServerFailed ) - { - printf( "Could not connect to JACK server.\n" ); - } + printf("jack_client_open() failed, status 0x%2.0x\n", status); + if (status & JackServerFailed) { printf("Could not connect to JACK server.\n"); } return false; } - if( status & JackNameNotUnique ) + if (status & JackNameNotUnique) { - printf( "there's already a client with name '%s', so unique " - "name '%s' was assigned\n", clientName. - toLatin1().constData(), - jack_get_client_name( m_client ) ); + printf( "there's already a client with name '%s', so unique " + "name '%s' was assigned\n", + clientName.toLatin1().constData(), jack_get_client_name(m_client)); } // set process-callback - jack_set_process_callback( m_client, staticProcessCallback, this ); + jack_set_process_callback(m_client, staticProcessCallback, this); // set shutdown-callback - jack_on_shutdown( m_client, shutdownCallback, this ); - - + jack_on_shutdown(m_client, shutdownCallback, this); - if( jack_get_sample_rate( m_client ) != sampleRate() ) - { - setSampleRate( jack_get_sample_rate( m_client ) ); - } + if (jack_get_sample_rate(m_client) != sampleRate()) { setSampleRate(jack_get_sample_rate(m_client)); } - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) + for (ch_cnt_t ch = 0; ch < channels(); ++ch) { - QString name = QString( "master out " ) + - ( ( ch % 2 ) ? "R" : "L" ) + - QString::number( ch / 2 + 1 ); - m_outputPorts.push_back( jack_port_register( m_client, - name.toLatin1().constData(), - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0 ) ); - if( m_outputPorts.back() == nullptr ) + QString name = QString("master out ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1); + m_outputPorts.push_back( + jack_port_register(m_client, name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0)); + if (m_outputPorts.back() == nullptr) { - printf( "no more JACK-ports available!\n" ); + printf("no more JACK-ports available!\n"); return false; } } @@ -203,51 +183,43 @@ bool AudioJack::initJackClient() void AudioJack::startProcessing() { - if( m_active || m_client == nullptr ) + if (m_active || m_client == nullptr) { m_stopped = false; return; } - if( jack_activate( m_client ) ) + if (jack_activate(m_client)) { - printf( "cannot activate client\n" ); + printf("cannot activate client\n"); return; } m_active = true; - // try to sync JACK's and LMMS's buffer-size -// jack_set_buffer_size( m_client, audioEngine()->framesPerPeriod() ); + // jack_set_buffer_size( m_client, audioEngine()->framesPerPeriod() ); - - - const char * * ports = jack_get_ports( m_client, nullptr, nullptr, - JackPortIsPhysical | - JackPortIsInput ); - if( ports == nullptr ) + const char** ports = jack_get_ports(m_client, nullptr, nullptr, JackPortIsPhysical | JackPortIsInput); + if (ports == nullptr) { - printf( "no physical playback ports. you'll have to do " - "connections at your own!\n" ); + printf("no physical playback ports. you'll have to do " + "connections at your own!\n"); } else { - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) + for (ch_cnt_t ch = 0; ch < channels(); ++ch) { - if( jack_connect( m_client, jack_port_name( - m_outputPorts[ch] ), - ports[ch] ) ) + if (jack_connect(m_client, jack_port_name(m_outputPorts[ch]), ports[ch])) { - printf( "cannot connect output ports. you'll " - "have to do connections at your own!\n" - ); + printf("cannot connect output ports. you'll " + "have to do connections at your own!\n"); } } } m_stopped = false; - free( ports ); + jack_free(ports); } @@ -263,14 +235,11 @@ void AudioJack::stopProcessing() void AudioJack::applyQualitySettings() { - if( hqAudio() ) + if (hqAudio()) { - setSampleRate( Engine::audioEngine()->processingSampleRate() ); + setSampleRate(Engine::audioEngine()->processingSampleRate()); - if( jack_get_sample_rate( m_client ) != sampleRate() ) - { - setSampleRate( jack_get_sample_rate( m_client ) ); - } + if (jack_get_sample_rate(m_client) != sampleRate()) { setSampleRate(jack_get_sample_rate(m_client)); } } AudioDevice::applyQualitySettings(); @@ -279,106 +248,91 @@ void AudioJack::applyQualitySettings() -void AudioJack::registerPort( AudioPort * _port ) +void AudioJack::registerPort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT // make sure, port is not already registered - unregisterPort( _port ); - const QString name[2] = { _port->name() + " L", - _port->name() + " R" } ; + unregisterPort(port); + const QString name[2] = {port->name() + " L", port->name() + " R"}; - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) { - m_portMap[_port].ports[ch] = jack_port_register( m_client, - name[ch].toLatin1().constData(), - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0 ); + m_portMap[port].ports[ch] = jack_port_register( + m_client, name[ch].toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); } +#else + (void)port; #endif } -void AudioJack::unregisterPort( AudioPort * _port ) +void AudioJack::unregisterPort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT - if( m_portMap.contains( _port ) ) + if (m_portMap.contains(port)) { - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) { - if( m_portMap[_port].ports[ch] != nullptr ) - { - jack_port_unregister( m_client, - m_portMap[_port].ports[ch] ); - } + if (m_portMap[port].ports[ch] != nullptr) { jack_port_unregister(m_client, m_portMap[port].ports[ch]); } } - m_portMap.erase( m_portMap.find( _port ) ); + m_portMap.erase(m_portMap.find(port)); } +#else + (void)port; #endif } - - - -void AudioJack::renamePort( AudioPort * _port ) +void AudioJack::renamePort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT - if( m_portMap.contains( _port ) ) + if (m_portMap.contains(port)) { - const QString name[2] = { _port->name() + " L", - _port->name() + " R" }; - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) + const QString name[2] = {port->name() + " L", port->name() + " R"}; + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) { #ifdef LMMS_HAVE_JACK_PRENAME - jack_port_rename( m_client, m_portMap[_port].ports[ch], - name[ch].toLatin1().constData() ); + jack_port_rename(m_client, m_portMap[port].ports[ch], name[ch].toLatin1().constData()); #else - jack_port_set_name( m_portMap[_port].ports[ch], - name[ch].toLatin1().constData() ); + jack_port_set_name(m_portMap[port].ports[ch], name[ch].toLatin1().constData()); #endif } } +#else + (void)port; #endif // AUDIO_PORT_SUPPORT } -int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) +int AudioJack::processCallback(jack_nframes_t nframes) { // do midi processing first so that midi input can // add to the following sound processing - if( m_midiClient && _nframes > 0 ) + if (m_midiClient && nframes > 0) { - m_midiClient.load()->JackMidiRead(_nframes); - m_midiClient.load()->JackMidiWrite(_nframes); + m_midiClient.load()->JackMidiRead(nframes); + m_midiClient.load()->JackMidiWrite(nframes); } - for( int c = 0; c < channels(); ++c ) + for (int c = 0; c < channels(); ++c) { - m_tempOutBufs[c] = - (jack_default_audio_sample_t *) jack_port_get_buffer( - m_outputPorts[c], _nframes ); + m_tempOutBufs[c] = (jack_default_audio_sample_t*)jack_port_get_buffer(m_outputPorts[c], nframes); } #ifdef AUDIO_PORT_SUPPORT - const int frames = std::min(_nframes, audioEngine()->framesPerPeriod()); - for( JackPortMap::iterator it = m_portMap.begin(); - it != m_portMap.end(); ++it ) + const int frames = std::min(nframes, audioEngine()->framesPerPeriod()); + for (JackPortMap::iterator it = m_portMap.begin(); it != m_portMap.end(); ++it) { - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) + for (ch_cnt_t ch = 0; ch < channels(); ++ch) { - if( it.value().ports[ch] == nullptr ) - { - continue; - } - jack_default_audio_sample_t * buf = - (jack_default_audio_sample_t *) jack_port_get_buffer( - it.value().ports[ch], - _nframes ); - for( int frame = 0; frame < frames; ++frame ) + if (it.value().ports[ch] == nullptr) { continue; } + jack_default_audio_sample_t* buf + = (jack_default_audio_sample_t*)jack_port_get_buffer(it.value().ports[ch], nframes); + for (int frame = 0; frame < frames; ++frame) { buf[frame] = it.key()->buffer()[frame][ch]; } @@ -387,28 +341,25 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) #endif jack_nframes_t done = 0; - while( done < _nframes && m_stopped == false ) + while (done < nframes && !m_stopped) { - jack_nframes_t todo = std::min( - _nframes, - m_framesToDoInCurBuf - - m_framesDoneInCurBuf); + jack_nframes_t todo = std::min(nframes - done, m_framesToDoInCurBuf - m_framesDoneInCurBuf); const float gain = audioEngine()->masterGain(); - for( int c = 0; c < channels(); ++c ) + for (int c = 0; c < channels(); ++c) { - jack_default_audio_sample_t * o = m_tempOutBufs[c]; - for( jack_nframes_t frame = 0; frame < todo; ++frame ) + jack_default_audio_sample_t* o = m_tempOutBufs[c]; + for (jack_nframes_t frame = 0; frame < todo; ++frame) { - o[done+frame] = m_outBuf[m_framesDoneInCurBuf+frame][c] * gain; + o[done + frame] = m_outBuf[m_framesDoneInCurBuf + frame][c] * gain; } } done += todo; m_framesDoneInCurBuf += todo; - if( m_framesDoneInCurBuf == m_framesToDoInCurBuf ) + if (m_framesDoneInCurBuf == m_framesToDoInCurBuf) { - m_framesToDoInCurBuf = getNextBuffer( m_outBuf ); + m_framesToDoInCurBuf = getNextBuffer(m_outBuf); m_framesDoneInCurBuf = 0; - if( !m_framesToDoInCurBuf ) + if (!m_framesToDoInCurBuf) { m_stopped = true; break; @@ -416,12 +367,12 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) } } - if( _nframes != done ) + if (nframes != done) { - for( int c = 0; c < channels(); ++c ) + for (int c = 0; c < channels(); ++c) { - jack_default_audio_sample_t * b = m_tempOutBufs[c] + done; - memset( b, 0, sizeof( *b ) * ( _nframes - done ) ); + jack_default_audio_sample_t* b = m_tempOutBufs[c] + done; + memset(b, 0, sizeof(*b) * (nframes - done)); } } @@ -431,52 +382,44 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) -int AudioJack::staticProcessCallback( jack_nframes_t _nframes, void * _udata ) +int AudioJack::staticProcessCallback(jack_nframes_t nframes, void* udata) { - return static_cast( _udata )-> - processCallback( _nframes, _udata ); + return static_cast(udata)->processCallback(nframes); } -void AudioJack::shutdownCallback( void * _udata ) +void AudioJack::shutdownCallback(void* udata) { - auto _this = static_cast(_udata); - _this->m_client = nullptr; - _this->zombified(); + auto thisClass = static_cast(udata); + thisClass->m_client = nullptr; + emit thisClass->zombified(); } - -AudioJack::setupWidget::setupWidget( QWidget * _parent ) : - AudioDeviceSetupWidget( AudioJack::name(), _parent ) +AudioJack::setupWidget::setupWidget(QWidget* parent) + : AudioDeviceSetupWidget(AudioJack::name(), parent) { - QString cn = ConfigManager::inst()->value( "audiojack", "clientname" ); - if( cn.isEmpty() ) - { - cn = "lmms"; - } - m_clientName = new QLineEdit( cn, this ); - m_clientName->setGeometry( 10, 20, 160, 20 ); + QFormLayout * form = new QFormLayout(this); - auto cn_lbl = new QLabel(tr("Client name"), this); - cn_lbl->setFont( pointSize<7>( cn_lbl->font() ) ); - cn_lbl->setGeometry( 10, 40, 160, 10 ); + QString cn = ConfigManager::inst()->value("audiojack", "clientname"); + if (cn.isEmpty()) { cn = "lmms"; } + m_clientName = new QLineEdit(cn, this); + + form->addRow(tr("Client name"), m_clientName); auto m = new gui::LcdSpinBoxModel(/* this */); - m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); - m->setStep( 2 ); - m->setValue( ConfigManager::inst()->value( "audiojack", - "channels" ).toInt() ); + m->setRange(DEFAULT_CHANNELS, SURROUND_CHANNELS); + m->setStep(2); + m->setValue(ConfigManager::inst()->value("audiojack", "channels").toInt()); - m_channels = new gui::LcdSpinBox( 1, this ); - m_channels->setModel( m ); - m_channels->setLabel( tr( "Channels" ) ); - m_channels->move( 180, 20 ); + m_channels = new gui::LcdSpinBox(1, this); + m_channels->setModel(m); + form->addRow(tr("Channels"), m_channels); } @@ -492,14 +435,11 @@ AudioJack::setupWidget::~setupWidget() void AudioJack::setupWidget::saveSettings() { - ConfigManager::inst()->setValue( "audiojack", "clientname", - m_clientName->text() ); - ConfigManager::inst()->setValue( "audiojack", "channels", - QString::number( m_channels->value() ) ); + ConfigManager::inst()->setValue("audiojack", "clientname", m_clientName->text()); + ConfigManager::inst()->setValue("audiojack", "channels", QString::number(m_channels->value())); } - } // namespace lmms #endif // LMMS_HAVE_JACK diff --git a/src/core/audio/AudioOss.cpp b/src/core/audio/AudioOss.cpp index 73969533fba..8fedd3b2b9a 100644 --- a/src/core/audio/AudioOss.cpp +++ b/src/core/audio/AudioOss.cpp @@ -27,7 +27,7 @@ #ifdef LMMS_HAVE_OSS #include -#include +#include #include #include "endian_handling.h" @@ -320,12 +320,11 @@ void AudioOss::run() AudioOss::setupWidget::setupWidget( QWidget * _parent ) : AudioDeviceSetupWidget( AudioOss::name(), _parent ) { + QFormLayout * form = new QFormLayout(this); + m_device = new QLineEdit( probeDevice(), this ); - m_device->setGeometry( 10, 20, 160, 20 ); - auto dev_lbl = new QLabel(tr("Device"), this); - dev_lbl->setFont( pointSize<7>( dev_lbl->font() ) ); - dev_lbl->setGeometry( 10, 40, 160, 10 ); + form->addRow(tr("Device"), m_device); auto m = new gui::LcdSpinBoxModel(/* this */); m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); @@ -335,9 +334,8 @@ AudioOss::setupWidget::setupWidget( QWidget * _parent ) : m_channels = new gui::LcdSpinBox( 1, this ); m_channels->setModel( m ); - m_channels->setLabel( tr( "Channels" ) ); - m_channels->move( 180, 20 ); + form->addRow(tr("Channels"), m_channels); } diff --git a/src/core/audio/AudioPortAudio.cpp b/src/core/audio/AudioPortAudio.cpp index c06eee3d4ad..3684a79a8d7 100644 --- a/src/core/audio/AudioPortAudio.cpp +++ b/src/core/audio/AudioPortAudio.cpp @@ -49,7 +49,7 @@ void AudioPortAudioSetupUtil::updateChannels() #ifdef LMMS_HAVE_PORTAUDIO -#include +#include #include "Engine.h" #include "ConfigManager.h" @@ -419,19 +419,13 @@ AudioPortAudio::setupWidget::setupWidget( QWidget * _parent ) : { using gui::ComboBox; - m_backend = new ComboBox( this, "BACKEND" ); - m_backend->setGeometry( 64, 15, 260, ComboBox::DEFAULT_HEIGHT ); + QFormLayout * form = new QFormLayout(this); - auto backend_lbl = new QLabel(tr("Backend"), this); - backend_lbl->setFont( pointSize<7>( backend_lbl->font() ) ); - backend_lbl->move( 8, 18 ); + m_backend = new ComboBox( this, "BACKEND" ); + form->addRow(tr("Backend"), m_backend); m_device = new ComboBox( this, "DEVICE" ); - m_device->setGeometry( 64, 35, 260, ComboBox::DEFAULT_HEIGHT ); - - auto dev_lbl = new QLabel(tr("Device"), this); - dev_lbl->setFont( pointSize<7>( dev_lbl->font() ) ); - dev_lbl->move( 8, 38 ); + form->addRow(tr("Device"), m_device); /* LcdSpinBoxModel * m = new LcdSpinBoxModel( ); m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); diff --git a/src/core/audio/AudioPulseAudio.cpp b/src/core/audio/AudioPulseAudio.cpp index 26a5a02e295..3ca8764cc47 100644 --- a/src/core/audio/AudioPulseAudio.cpp +++ b/src/core/audio/AudioPulseAudio.cpp @@ -22,8 +22,8 @@ * */ +#include #include -#include #include "AudioPulseAudio.h" @@ -312,24 +312,21 @@ void AudioPulseAudio::signalConnected( bool connected ) AudioPulseAudio::setupWidget::setupWidget( QWidget * _parent ) : AudioDeviceSetupWidget( AudioPulseAudio::name(), _parent ) { - m_device = new QLineEdit( AudioPulseAudio::probeDevice(), this ); - m_device->setGeometry( 10, 20, 160, 20 ); + QFormLayout * form = new QFormLayout(this); - auto dev_lbl = new QLabel(tr("Device"), this); - dev_lbl->setFont( pointSize<7>( dev_lbl->font() ) ); - dev_lbl->setGeometry( 10, 40, 160, 10 ); + m_device = new QLineEdit( AudioPulseAudio::probeDevice(), this ); + form->addRow(tr("Device"), m_device); - auto m = new gui::LcdSpinBoxModel(/* this */); + auto m = new gui::LcdSpinBoxModel(); m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); m->setStep( 2 ); m->setValue( ConfigManager::inst()->value( "audiopa", - "channels" ).toInt() ); + "channels" ).toInt() ); m_channels = new gui::LcdSpinBox( 1, this ); m_channels->setModel( m ); - m_channels->setLabel( tr( "Channels" ) ); - m_channels->move( 180, 20 ); + form->addRow(tr("Channels"), m_channels); } diff --git a/src/core/audio/AudioSdl.cpp b/src/core/audio/AudioSdl.cpp index c5ffa64a942..12aa97d6385 100644 --- a/src/core/audio/AudioSdl.cpp +++ b/src/core/audio/AudioSdl.cpp @@ -26,7 +26,7 @@ #ifdef LMMS_HAVE_SDL -#include +#include #include #include @@ -327,14 +327,12 @@ void AudioSdl::sdlInputAudioCallback(Uint8 *_buf, int _len) { AudioSdl::setupWidget::setupWidget( QWidget * _parent ) : AudioDeviceSetupWidget( AudioSdl::name(), _parent ) { + QFormLayout * form = new QFormLayout(this); + QString dev = ConfigManager::inst()->value( "audiosdl", "device" ); m_device = new QLineEdit( dev, this ); - m_device->setGeometry( 10, 20, 160, 20 ); - - auto dev_lbl = new QLabel(tr("Device"), this); - dev_lbl->setFont( pointSize<7>( dev_lbl->font() ) ); - dev_lbl->setGeometry( 10, 40, 160, 10 ); + form->addRow(tr("Device"), m_device); } diff --git a/src/core/audio/AudioSndio.cpp b/src/core/audio/AudioSndio.cpp index 0e46d08f6d1..bb9b249f87b 100644 --- a/src/core/audio/AudioSndio.cpp +++ b/src/core/audio/AudioSndio.cpp @@ -28,7 +28,7 @@ #ifdef LMMS_HAVE_SNDIO #include -#include +#include #include #include "endian_handling.h" @@ -183,12 +183,10 @@ void AudioSndio::run() AudioSndio::setupWidget::setupWidget( QWidget * _parent ) : AudioDeviceSetupWidget( AudioSndio::name(), _parent ) { - m_device = new QLineEdit( "", this ); - m_device->setGeometry( 10, 20, 160, 20 ); + QFormLayout * form = new QFormLayout(this); - QLabel * dev_lbl = new QLabel( tr( "Device" ), this ); - dev_lbl->setFont( pointSize<6>( dev_lbl->font() ) ); - dev_lbl->setGeometry( 10, 40, 160, 10 ); + m_device = new QLineEdit( "", this ); + form->addRow(tr("Device"), m_device); gui::LcdSpinBoxModel * m = new gui::LcdSpinBoxModel( /* this */ ); m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); @@ -198,9 +196,8 @@ AudioSndio::setupWidget::setupWidget( QWidget * _parent ) : m_channels = new gui::LcdSpinBox( 1, this ); m_channels->setModel( m ); - m_channels->setLabel( tr( "Channels" ) ); - m_channels->move( 180, 20 ); + form->addRow(tr("Channels"), m_channels); } diff --git a/src/core/audio/AudioSoundIo.cpp b/src/core/audio/AudioSoundIo.cpp index 556909a843d..36a1929dfdf 100644 --- a/src/core/audio/AudioSoundIo.cpp +++ b/src/core/audio/AudioSoundIo.cpp @@ -26,7 +26,7 @@ #ifdef LMMS_HAVE_SOUNDIO -#include +#include #include #include "Engine.h" @@ -451,19 +451,13 @@ AudioSoundIo::setupWidget::setupWidget( QWidget * _parent ) : { m_setupUtil.m_setupWidget = this; - m_backend = new gui::ComboBox( this, "BACKEND" ); - m_backend->setGeometry( 64, 15, 260, 20 ); + QFormLayout * form = new QFormLayout(this); - QLabel * backend_lbl = new QLabel( tr( "Backend" ), this ); - backend_lbl->setFont( pointSize<7>( backend_lbl->font() ) ); - backend_lbl->move( 8, 18 ); + m_backend = new gui::ComboBox( this, "BACKEND" ); + form->addRow(tr("Backend"), m_backend); m_device = new gui::ComboBox( this, "DEVICE" ); - m_device->setGeometry( 64, 35, 260, 20 ); - - QLabel * dev_lbl = new QLabel( tr( "Device" ), this ); - dev_lbl->setFont( pointSize<7>( dev_lbl->font() ) ); - dev_lbl->move( 8, 38 ); + form->addRow(tr("Device"), m_device); // Setup models m_soundio = soundio_create(); diff --git a/src/core/lv2/Lv2Features.cpp b/src/core/lv2/Lv2Features.cpp index c8fc0546517..25ee115443a 100644 --- a/src/core/lv2/Lv2Features.cpp +++ b/src/core/lv2/Lv2Features.cpp @@ -109,7 +109,12 @@ void *&Lv2Features::operator[](const char *featName) void Lv2Features::clear() { - m_featureByUri.clear(); + m_features.clear(); + for (auto& [uri, feature] : m_featureByUri) + { + (void) uri; + feature = nullptr; + } } diff --git a/src/core/lv2/Lv2Options.cpp b/src/core/lv2/Lv2Options.cpp index 36281ee6345..7b4528cb606 100644 --- a/src/core/lv2/Lv2Options.cpp +++ b/src/core/lv2/Lv2Options.cpp @@ -94,6 +94,16 @@ void Lv2Options::initOption(LV2_URID key, uint32_t size, LV2_URID type, } + + +void Lv2Options::clear() +{ + m_options.clear(); + m_optionValues.clear(); + m_optionByUrid.clear(); +} + + } // namespace lmms #endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 11290013e5b..158196fdbd2 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -45,6 +45,7 @@ #include "Lv2Evbuf.h" #include "MidiEvent.h" #include "MidiEventToByteSeq.h" +#include "NoCopyNoMove.h" namespace lmms @@ -168,6 +169,27 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin, +class Lv2ProcSuspender : NoCopyNoMove +{ +public: + Lv2ProcSuspender(Lv2Proc* proc) + : m_proc(proc) + , m_wasActive(proc->m_instance) + { + if (m_wasActive) { m_proc->shutdownPlugin(); } + } + ~Lv2ProcSuspender() + { + if (m_wasActive) { m_proc->initPlugin(); } + } +private: + Lv2Proc* const m_proc; + const bool m_wasActive; +}; + + + + Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : LinkedModelGroup(parent), m_plugin(plugin), @@ -175,6 +197,7 @@ Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : m_midiInputBuf(m_maxMidiInputEvents), m_midiInputReader(m_midiInputBuf) { + createPorts(); initPlugin(); } @@ -186,22 +209,7 @@ Lv2Proc::~Lv2Proc() { shutdownPlugin(); } -void Lv2Proc::reload() -{ - // save controls, which we want to keep - QDomDocument doc; - QDomElement controls = doc.createElement("controls"); - saveValues(doc, controls); - // backup construction variables - const LilvPlugin* plugin = m_plugin; - Model* parent = Model::parentModel(); - // destroy everything using RAII ... - this->~Lv2Proc(); - // ... and reuse it ("placement new") - new (this) Lv2Proc(plugin, parent); - // reload the controls - loadValues(controls); -} +void Lv2Proc::reload() { Lv2ProcSuspender(this); } @@ -434,19 +442,22 @@ void Lv2Proc::initPlugin() initPluginSpecificFeatures(); m_features.createFeatureVectors(); - createPorts(); - m_instance = lilv_plugin_instantiate(m_plugin, Engine::audioEngine()->processingSampleRate(), m_features.featurePointers()); if (m_instance) { - if(m_worker) { + const auto iface = static_cast( + lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface)); + if (iface) { m_worker->setHandle(lilv_instance_get_handle(m_instance)); + m_worker->setInterface(iface); } for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum) + { connectPort(portNum); + } lilv_instance_activate(m_instance); } else @@ -472,6 +483,7 @@ void Lv2Proc::shutdownPlugin() m_instance = nullptr; m_features.clear(); + m_options.clear(); } m_valid = true; } @@ -528,12 +540,10 @@ void Lv2Proc::initPluginSpecificFeatures() // 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_worker.emplace(&m_workLock, threaded); m_features[LV2_WORKER__schedule] = m_worker->feature(); - // Note: m_worker::setHandle will still need to be called later + // note: the worker interface can not be instantiated yet - it requires m_instance. see initPlugin() } } diff --git a/src/core/lv2/Lv2Worker.cpp b/src/core/lv2/Lv2Worker.cpp index 5af955ff766..c763bacad97 100644 --- a/src/core/lv2/Lv2Worker.cpp +++ b/src/core/lv2/Lv2Worker.cpp @@ -60,10 +60,7 @@ std::size_t Lv2Worker::bufferSize() const -Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, - Semaphore* common_work_lock, - bool threaded) : - m_iface(iface), +Lv2Worker::Lv2Worker(Semaphore* commonWorkLock, bool threaded) : m_threaded(threaded), m_response(bufferSize()), m_requests(bufferSize()), @@ -71,9 +68,8 @@ Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, m_requestsReader(m_requests), m_responsesReader(m_responses), m_sem(0), - m_workLock(common_work_lock) + m_workLock(commonWorkLock) { - 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 @@ -91,6 +87,24 @@ Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, +void Lv2Worker::setHandle(LV2_Handle handle) +{ + assert(handle); + m_handle = handle; +} + + + + +void Lv2Worker::setInterface(const LV2_Worker_Interface* newInterface) +{ + assert(newInterface); + m_interface = newInterface; +} + + + + Lv2Worker::~Lv2Worker() { m_exit = true; @@ -120,7 +134,9 @@ LV2_Worker_Status Lv2Worker::respond(uint32_t size, const void* data) } else { - m_iface->work_response(m_handle, size, data); + assert(m_handle); + assert(m_interface); + m_interface->work_response(m_handle, size, data); } return LV2_WORKER_SUCCESS; } @@ -136,6 +152,7 @@ void Lv2Worker::workerFunc() 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) @@ -144,8 +161,10 @@ void Lv2Worker::workerFunc() if(size > buf.size()) { buf.resize(size); } if(size) { m_requestsReader.read(size).copy(buf.data(), size); } + assert(m_handle); + assert(m_interface); m_workLock->wait(); - m_iface->work(m_handle, staticWorkerRespond, this, size, buf.data()); + m_interface->work(m_handle, staticWorkerRespond, this, size, buf.data()); m_workLock->post(); } } @@ -172,9 +191,11 @@ LV2_Worker_Status Lv2Worker::scheduleWork(uint32_t size, const void *data) } else { + assert(m_handle); + assert(m_interface); // Execute work immediately in this thread m_workLock->wait(); - m_iface->work(m_handle, staticWorkerRespond, this, size, data); + m_interface->work(m_handle, staticWorkerRespond, this, size, data); m_workLock->post(); } @@ -189,10 +210,13 @@ void Lv2Worker::emitResponses() { std::size_t read_space = m_responsesReader.read_space(); uint32_t size; - while (read_space > sizeof(size)) { + while (read_space > sizeof(size)) + { + assert(m_handle); + assert(m_interface); 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()); + m_interface->work_response(m_handle, size, m_response.data()); read_space -= sizeof(size) + size; } } diff --git a/src/gui/AudioAlsaSetupWidget.cpp b/src/gui/AudioAlsaSetupWidget.cpp index 4ea6d4c5855..7db822b4be8 100644 --- a/src/gui/AudioAlsaSetupWidget.cpp +++ b/src/gui/AudioAlsaSetupWidget.cpp @@ -23,7 +23,7 @@ */ #include -#include +#include #include "AudioAlsaSetupWidget.h" @@ -40,6 +40,8 @@ AudioAlsaSetupWidget::AudioAlsaSetupWidget( QWidget * _parent ) : AudioDeviceSetupWidget( AudioAlsa::name(), _parent ), m_selectedDevice(-1) { + QFormLayout * form = new QFormLayout(this); + m_deviceInfos = AudioAlsa::getAvailableDevices(); QString deviceText = ConfigManager::inst()->value( "audioalsa", "device" ); @@ -62,14 +64,11 @@ AudioAlsaSetupWidget::AudioAlsaSetupWidget( QWidget * _parent ) : m_selectedDevice = m_deviceComboBox->currentIndex(); - m_deviceComboBox->setGeometry( 10, 20, 160, 20 ); connect(m_deviceComboBox, SIGNAL(currentIndexChanged(int)), SLOT(onCurrentIndexChanged(int))); - auto dev_lbl = new QLabel(tr("DEVICE"), this); - dev_lbl->setFont( pointSize<7>( dev_lbl->font() ) ); - dev_lbl->setGeometry( 10, 40, 160, 10 ); + form->addRow(tr("Device"), m_deviceComboBox); auto m = new LcdSpinBoxModel(/* this */); m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); @@ -79,9 +78,8 @@ AudioAlsaSetupWidget::AudioAlsaSetupWidget( QWidget * _parent ) : m_channels = new LcdSpinBox( 1, this ); m_channels->setModel( m ); - m_channels->setLabel( tr( "CHANNELS" ) ); - m_channels->move( 180, 20 ); + form->addRow(tr("Channels"), m_channels); } diff --git a/src/gui/AudioDeviceSetupWidget.cpp b/src/gui/AudioDeviceSetupWidget.cpp index b78800cecb7..98d03638f3c 100644 --- a/src/gui/AudioDeviceSetupWidget.cpp +++ b/src/gui/AudioDeviceSetupWidget.cpp @@ -28,7 +28,7 @@ namespace lmms::gui { AudioDeviceSetupWidget::AudioDeviceSetupWidget(const QString & caption, QWidget * parent) : - TabWidget(TabWidget::tr("Settings for %1").arg(tr(caption.toUtf8())), parent) + QGroupBox(QGroupBox::tr("Settings for %1").arg(tr(caption.toUtf8())), parent) { } @@ -38,4 +38,4 @@ void AudioDeviceSetupWidget::show() QWidget::show(); } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index afed153f928..e050d14bda6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -14,6 +14,7 @@ SET(LMMS_SRCS gui/EffectView.cpp gui/embed.cpp gui/FileBrowser.cpp + gui/FileBrowserSearcher.cpp gui/GuiApplication.cpp gui/LadspaControlView.cpp gui/LfoControllerDialog.cpp @@ -94,11 +95,13 @@ SET(LMMS_SRCS gui/widgets/AutomatableButton.cpp gui/widgets/AutomatableSlider.cpp + gui/widgets/BarModelEditor.cpp gui/widgets/CPULoadWidget.cpp gui/widgets/CaptionMenu.cpp gui/widgets/ComboBox.cpp gui/widgets/CustomTextKnob.cpp gui/widgets/Fader.cpp + gui/widgets/FloatModelEditorBase.cpp gui/widgets/Graph.cpp gui/widgets/GroupBox.cpp gui/widgets/Knob.cpp @@ -115,6 +118,7 @@ SET(LMMS_SRCS gui/widgets/SimpleTextFloat.cpp gui/widgets/TabBar.cpp gui/widgets/TabWidget.cpp + gui/widgets/TempoSyncBarModelEditor.cpp gui/widgets/TempoSyncKnob.cpp gui/widgets/TextFloat.cpp gui/widgets/TimeDisplayWidget.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 181e67cd749..32f29988b83 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -23,8 +23,11 @@ * */ +#include "FileBrowser.h" + #include #include +#include #include #include #include @@ -36,7 +39,10 @@ #include #include +#include + #include "FileBrowser.h" +#include "FileBrowserSearcher.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "DataFile.h" @@ -96,14 +102,13 @@ void FileBrowser::addContentCheckBox() FileBrowser::FileBrowser(const QString & directories, const QString & filter, const QString & title, const QPixmap & pm, - QWidget * parent, bool dirs_as_items, bool recurse, + QWidget * parent, bool dirs_as_items, const QString& userDir, const QString& factoryDir): SideBarWidget( title, pm, parent ), m_directories( directories ), m_filter( filter ), m_dirsAsItems( dirs_as_items ), - m_recurse( recurse ), m_userDir(userDir), m_factoryDir(factoryDir) { @@ -121,11 +126,12 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, searchWidgetLayout->setContentsMargins(0, 0, 0, 0); searchWidgetLayout->setSpacing( 0 ); - m_filterEdit = new QLineEdit( searchWidget ); - m_filterEdit->setPlaceholderText( tr("Search") ); - m_filterEdit->setClearButtonEnabled( true ); - connect( m_filterEdit, SIGNAL( textEdited( const QString& ) ), - this, SLOT( filterAndExpandItems( const QString& ) ) ); + m_filterEdit = new QLineEdit(searchWidget); + m_filterEdit->setPlaceholderText(tr("Search")); + m_filterEdit->setClearButtonEnabled(true); + m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); + + connect(m_filterEdit, &QLineEdit::textEdited, this, &FileBrowser::onSearch); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); @@ -140,6 +146,19 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_fileBrowserTreeWidget = new FileBrowserTreeWidget( contentParent() ); addContentWidget( m_fileBrowserTreeWidget ); + m_searchTreeWidget = new FileBrowserTreeWidget(contentParent()); + m_searchTreeWidget->hide(); + addContentWidget(m_searchTreeWidget); + + auto searchTimer = new QTimer(this); + connect(searchTimer, &QTimer::timeout, this, &FileBrowser::buildSearchTree); + searchTimer->start(FileBrowserSearcher::MillisecondsPerMatch); + + m_searchIndicator = new QProgressBar(this); + m_searchIndicator->setMinimum(0); + m_searchIndicator->setMaximum(100); + addContentWidget(m_searchIndicator); + // Whenever the FileBrowser has focus, Ctrl+F should direct focus to its filter box. auto filterFocusShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this, SLOT(giveFocusToFilter())); filterFocusShortcut->setContext(Qt::WidgetWithChildrenShortcut); @@ -157,86 +176,122 @@ void FileBrowser::saveDirectoriesStates() void FileBrowser::restoreDirectoriesStates() { - expandItems(nullptr, m_savedExpandedDirs); + expandItems(m_savedExpandedDirs); } -bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item) +void FileBrowser::buildSearchTree() { - // Call with item = nullptr to filter the entire tree + if (!m_currentSearch) { return; } - if (item == nullptr) + const auto match = m_currentSearch->match(); + using State = FileBrowserSearcher::SearchFuture::State; + if ((m_currentSearch->state() == State::Completed && match.isEmpty()) + || m_currentSearch->state() == State::Cancelled) { - // First search character so need to save current expanded directories - if (m_previousFilterValue.isEmpty()) - { - saveDirectoriesStates(); - } - - m_previousFilterValue = filter; + m_currentSearch = nullptr; + m_searchIndicator->setMaximum(100); + return; } + else if (match.isEmpty()) { return; } - if (filter.isEmpty()) + auto basePath = QString{}; + for (const auto& path : m_directories.split('*')) { - // Restore previous expanded directories - if (item == nullptr) - { - restoreDirectoriesStates(); - } - - return false; + if (!match.startsWith(QDir{path}.absolutePath())) { continue; } + basePath = path; + break; } - - bool anyMatched = false; - int numChildren = item ? item->childCount() : m_fileBrowserTreeWidget->topLevelItemCount(); + if (basePath.isEmpty()) { return; } - for (int i = 0; i < numChildren; ++i) + const auto baseDir = QDir{basePath}; + const auto matchInfo = QFileInfo{match}; + const auto matchRelativeToBasePath = baseDir.relativeFilePath(match); + + auto pathParts = QDir::cleanPath(matchRelativeToBasePath).split("/"); + auto currentItem = static_cast(nullptr); + auto currentDir = baseDir; + + for (const auto& pathPart : pathParts) { - QTreeWidgetItem * it = item ? item->child( i ) : m_fileBrowserTreeWidget->topLevelItem(i); + auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount(); + auto childItem = static_cast(nullptr); - auto d = dynamic_cast(it); - if (d) + for (int i = 0; i < childCount; ++i) { - if (it->text(0).contains(filter, Qt::CaseInsensitive)) + auto item = currentItem ? currentItem->child(i) : m_searchTreeWidget->topLevelItem(i); + if (item->text(0) == pathPart) { - it->setHidden(false); - it->setExpanded(true); - filterAndExpandItems(QString(), it); - anyMatched = true; + childItem = item; + break; } - else - { - // 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; - } } - else + if (!childItem) { - auto f = dynamic_cast(it); - if (f) + auto pathPartInfo = QFileInfo(currentDir, pathPart); + if (pathPartInfo.isDir()) { - // File - bool didMatch = it->text(0).contains(filter, Qt::CaseInsensitive); - it->setHidden(!didMatch); - anyMatched = anyMatched || didMatch; + // Only update directory (i.e., add entries) when it is the matched directory (so do not update + // parents since entries would be added to them that did not match the filter) + const auto disablePopulation = pathParts.indexOf(pathPart) < pathParts.size() - 1; + + auto item = new Directory(pathPart, currentDir.path(), m_filter, disablePopulation); + currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item); + item->update(); + if (disablePopulation) { m_searchTreeWidget->expandItem(item); } + childItem = item; } - - // A standard item (i.e. no file or directory item?) else { - // Hide if there's any filter - it->setHidden(!filter.isEmpty()); + auto item = new FileItem(pathPart, currentDir.path()); + currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item); + childItem = item; } } + + currentItem = childItem; + if (!currentDir.cd(pathPart)) { break; } + } +} + + +void FileBrowser::onSearch(const QString& filter) +{ + if (filter.isEmpty()) + { + toggleSearch(false); + FileBrowserSearcher::instance()->cancel(); + return; } - return anyMatched; + auto directories = m_directories.split('*'); + if (m_showUserContent && !m_showUserContent->isChecked()) { directories.removeAll(m_userDir); } + if (m_showFactoryContent && !m_showFactoryContent->isChecked()) { directories.removeAll(m_factoryDir); } + if (directories.isEmpty()) { return; } + + m_searchTreeWidget->clear(); + toggleSearch(true); + + auto browserExtensions = m_filter; + const auto searchExtensions = browserExtensions.remove("*.").split(' '); + m_currentSearch = FileBrowserSearcher::instance()->search(filter, directories, searchExtensions); +} + +void FileBrowser::toggleSearch(bool on) +{ + if (on) + { + m_searchTreeWidget->show(); + m_fileBrowserTreeWidget->hide(); + m_searchIndicator->setMaximum(0); + return; + } + + m_searchTreeWidget->hide(); + m_fileBrowserTreeWidget->show(); + m_searchIndicator->setMaximum(100); } @@ -275,14 +330,16 @@ void FileBrowser::reloadTree() } else { - filterAndExpandItems(m_filterEdit->text()); + onSearch(m_filterEdit->text()); } } -void FileBrowser::expandItems(QTreeWidgetItem* item, QList expandedDirs) +void FileBrowser::expandItems(const QList& expandedDirs, QTreeWidgetItem* item) { + if (expandedDirs.isEmpty()) { return; } + int numChildren = item ? item->childCount() : m_fileBrowserTreeWidget->topLevelItemCount(); for (int i = 0; i < numChildren; ++i) { @@ -290,14 +347,10 @@ void FileBrowser::expandItems(QTreeWidgetItem* item, QList expandedDirs auto d = dynamic_cast(it); if (d) { - // 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()) + if (it->childCount() > 0) { - expandItems(it, expandedDirs); + expandItems(expandedDirs, it); } } @@ -321,6 +374,8 @@ void FileBrowser::giveFocusToFilter() void FileBrowser::addItems(const QString & path ) { + if (FileBrowser::directoryBlacklist().contains(path)) { return; } + if( m_dirsAsItems ) { m_fileBrowserTreeWidget->addTopLevelItem( new Directory( path, QString(), m_filter ) ); @@ -328,68 +383,63 @@ void FileBrowser::addItems(const QString & path ) } // try to add all directories from file system alphabetically into the tree - QDir cdir( path ); - QStringList files = cdir.entryList( QDir::Dirs, QDir::Name ); - files.sort(Qt::CaseInsensitive); - for( QStringList::const_iterator it = files.constBegin(); - it != files.constEnd(); ++it ) + QDir cdir(path); + if (!cdir.isReadable()) { return; } + QFileInfoList entries = cdir.entryInfoList( + m_filter.split(' '), dirFilters(), QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + for (const auto& entry : entries) { - QString cur_file = *it; - if( cur_file[0] != '.' ) + if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + + QString fileName = entry.fileName(); + if (entry.isDir()) { + // Merge dir's together bool orphan = true; - for( int i = 0; i < m_fileBrowserTreeWidget->topLevelItemCount(); ++i ) + for (int i = 0; i < m_fileBrowserTreeWidget->topLevelItemCount(); ++i) { auto d = dynamic_cast(m_fileBrowserTreeWidget->topLevelItem(i)); - if( d == nullptr || cur_file < d->text( 0 ) ) + if (d == nullptr || fileName < d->text(0)) { // insert before item, we're done - auto dd = new Directory(cur_file, path, m_filter); - m_fileBrowserTreeWidget->insertTopLevelItem( i,dd ); + auto dd = new Directory(fileName, path, m_filter); + m_fileBrowserTreeWidget->insertTopLevelItem(i,dd); dd->update(); // add files to the directory orphan = false; break; } - else if( cur_file == d->text( 0 ) ) + else if (fileName == d->text(0)) { // imagine we have subdirs named "TripleOscillator/xyz" in // two directories from m_directories // then only add one tree widget for both // so we don't add a new Directory - we just // add the path to the current directory - d->addDirectory( path ); + d->addDirectory(path); d->update(); orphan = false; break; } } - if( orphan ) + if (orphan) { // it has not yet been added yet, so it's (lexically) // larger than all other dirs => append it at the bottom - auto d = new Directory(cur_file, path, m_filter); + auto d = new Directory(fileName, path, m_filter); d->update(); - m_fileBrowserTreeWidget->addTopLevelItem( d ); + m_fileBrowserTreeWidget->addTopLevelItem(d); } } - } - - files = cdir.entryList( QDir::Files, QDir::Name ); - for( QStringList::const_iterator it = files.constBegin(); - it != files.constEnd(); ++it ) - { - QString cur_file = *it; - if( cur_file[0] != '.' ) + else if (entry.isFile()) { // TODO: don't insert instead of removing, order changed // remove existing file-items - QList existing = m_fileBrowserTreeWidget->findItems( - cur_file, Qt::MatchFixedString ); - if( !existing.empty() ) + QList existing = m_fileBrowserTreeWidget->findItems(fileName, Qt::MatchFixedString); + if (!existing.empty()) { delete existing.front(); } - (void) new FileItem( m_fileBrowserTreeWidget, cur_file, path ); + (void) new FileItem(m_fileBrowserTreeWidget, fileName, path); } } } @@ -958,74 +1008,27 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item ) } } - - - - - -QPixmap * Directory::s_folderPixmap = nullptr; -QPixmap * Directory::s_folderOpenedPixmap = nullptr; -QPixmap * Directory::s_folderLockedPixmap = nullptr; - - -Directory::Directory(const QString & filename, const QString & path, - const QString & filter ) : - QTreeWidgetItem( QStringList( filename ), TypeDirectoryItem ), - m_directories( path ), - m_filter( filter ), - m_dirCount( 0 ) +Directory::Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation) + : QTreeWidgetItem(QStringList(filename), TypeDirectoryItem) + , m_directories(path) + , m_filter(filter) + , m_dirCount(0) + , m_disableEntryPopulation(disableEntryPopulation) { - initPixmaps(); - + setIcon(0, !QDir{fullName()}.isReadable() ? m_folderLockedPixmap : m_folderPixmap); setChildIndicatorPolicy( QTreeWidgetItem::ShowIndicator ); - - if( !QDir( fullName() ).isReadable() ) - { - setIcon( 0, *s_folderLockedPixmap ); - } - else - { - setIcon( 0, *s_folderPixmap ); - } -} - - - - -void Directory::initPixmaps() -{ - if( s_folderPixmap == nullptr ) - { - s_folderPixmap = new QPixmap( - embed::getIconPixmap( "folder" ) ); - } - - if( s_folderOpenedPixmap == nullptr ) - { - s_folderOpenedPixmap = new QPixmap( - embed::getIconPixmap( "folder_opened" ) ); - } - - if( s_folderLockedPixmap == nullptr ) - { - s_folderLockedPixmap = new QPixmap( - embed::getIconPixmap( "folder_locked" ) ); - } } - - - void Directory::update() { if( !isExpanded() ) { - setIcon( 0, *s_folderPixmap ); + setIcon(0, m_folderPixmap); return; } - setIcon( 0, *s_folderOpenedPixmap ); - if( !childCount() ) + setIcon(0, m_folderOpenedPixmap); + if (!m_disableEntryPopulation && !childCount()) { m_dirCount = 0; // for all paths leading here, add their items @@ -1058,14 +1061,19 @@ void Directory::update() bool Directory::addItems(const QString& path) { + if (FileBrowser::directoryBlacklist().contains(path)) { return false; } + QDir thisDir(path); if (!thisDir.isReadable()) { return false; } treeWidget()->setUpdatesEnabled(false); - QFileInfoList entries = thisDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDir::LocaleAware | QDir::DirsFirst | QDir::Name); - for (auto& entry : entries) + QFileInfoList entries + = thisDir.entryInfoList(m_filter.split(' '), FileBrowser::dirFilters(), FileBrowser::sortFlags()); + for (const auto& entry : entries) { + if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + QString fileName = entry.fileName(); if (entry.isDir()) { @@ -1073,7 +1081,7 @@ bool Directory::addItems(const QString& path) addChild(dir); m_dirCount++; } - else if (entry.isFile() && thisDir.match(m_filter, fileName.toLower())) + else if (entry.isFile()) { auto fileItem = new FileItem(fileName, path); addChild(fileItem); @@ -1089,15 +1097,6 @@ bool Directory::addItems(const QString& path) -QPixmap * FileItem::s_projectFilePixmap = nullptr; -QPixmap * FileItem::s_presetFilePixmap = nullptr; -QPixmap * FileItem::s_sampleFilePixmap = nullptr; -QPixmap * FileItem::s_soundfontFilePixmap = nullptr; -QPixmap * FileItem::s_vstPluginFilePixmap = nullptr; -QPixmap * FileItem::s_midiFilePixmap = nullptr; -QPixmap * FileItem::s_unknownFilePixmap = nullptr; - - FileItem::FileItem(QTreeWidget * parent, const QString & name, const QString & path ) : QTreeWidgetItem( parent, QStringList( name) , TypeFileItem ), @@ -1123,72 +1122,38 @@ FileItem::FileItem(const QString & name, const QString & path ) : void FileItem::initPixmaps() { - if( s_projectFilePixmap == nullptr ) - { - s_projectFilePixmap = new QPixmap( embed::getIconPixmap( - "project_file", 16, 16 ) ); - } - - if( s_presetFilePixmap == nullptr ) - { - s_presetFilePixmap = new QPixmap( embed::getIconPixmap( - "preset_file", 16, 16 ) ); - } - - if( s_sampleFilePixmap == nullptr ) - { - s_sampleFilePixmap = new QPixmap( embed::getIconPixmap( - "sample_file", 16, 16 ) ); - } - - if ( s_soundfontFilePixmap == nullptr ) - { - s_soundfontFilePixmap = new QPixmap( embed::getIconPixmap( - "soundfont_file", 16, 16 ) ); - } - - if ( s_vstPluginFilePixmap == nullptr ) - { - s_vstPluginFilePixmap = new QPixmap( embed::getIconPixmap( - "vst_plugin_file", 16, 16 ) ); - } - - if( s_midiFilePixmap == nullptr ) - { - s_midiFilePixmap = new QPixmap( embed::getIconPixmap( - "midi_file", 16, 16 ) ); - } - - if( s_unknownFilePixmap == nullptr ) - { - s_unknownFilePixmap = new QPixmap( embed::getIconPixmap( - "unknown_file" ) ); - } + static auto s_projectFilePixmap = embed::getIconPixmap("project_file", 16, 16); + static auto s_presetFilePixmap = embed::getIconPixmap("preset_file", 16, 16); + static auto s_sampleFilePixmap = embed::getIconPixmap("sample_file", 16, 16); + static auto s_soundfontFilePixmap = embed::getIconPixmap("soundfont_file", 16, 16); + static auto s_vstPluginFilePixmap = embed::getIconPixmap("vst_plugin_file", 16, 16); + static auto s_midiFilePixmap = embed::getIconPixmap("midi_file", 16, 16); + static auto s_unknownFilePixmap = embed::getIconPixmap("unknown_file"); switch( m_type ) { case FileType::Project: - setIcon( 0, *s_projectFilePixmap ); + setIcon(0, s_projectFilePixmap); break; case FileType::Preset: - setIcon( 0, *s_presetFilePixmap ); + setIcon(0, s_presetFilePixmap); break; case FileType::SoundFont: - setIcon( 0, *s_soundfontFilePixmap ); + setIcon(0, s_soundfontFilePixmap); break; case FileType::VstPlugin: - setIcon( 0, *s_vstPluginFilePixmap ); + setIcon(0, s_vstPluginFilePixmap); break; case FileType::Sample: case FileType::Patch: // TODO - setIcon( 0, *s_sampleFilePixmap ); + setIcon(0, s_sampleFilePixmap); break; case FileType::Midi: - setIcon( 0, *s_midiFilePixmap ); + setIcon(0, s_midiFilePixmap); break; case FileType::Unknown: default: - setIcon( 0, *s_unknownFilePixmap ); + setIcon(0, s_unknownFilePixmap); break; } } @@ -1277,5 +1242,30 @@ QString FileItem::extension(const QString & file ) return QFileInfo( file ).suffix().toLower(); } +QString FileItem::defaultFilters() +{ + const auto projectFilters = QStringList{"*.mmp", "*.mpt", "*.mmpz"}; + const auto presetFilters = QStringList{"*.xpf", "*.xml", "*.xiz", "*.lv2"}; + const auto soundFontFilters = QStringList{"*.sf2", "*.sf3"}; + const auto patchFilters = QStringList{"*.pat"}; + const auto midiFilters = QStringList{"*.mid", "*.midi", "*.rmi"}; + + auto vstPluginFilters = QStringList{"*.dll"}; +#ifdef LMMS_BUILD_LINUX + vstPluginFilters.append("*.so"); +#endif + + auto audioFilters + = QStringList{"*.wav", "*.ogg", "*.ds", "*.flac", "*.spx", "*.voc", "*.aif", "*.aiff", "*.au", "*.raw"}; +#ifdef LMMS_HAVE_SNDFILE_MP3 + audioFilters.append("*.mp3"); +#endif + + const auto extensions = projectFilters + presetFilters + soundFontFilters + patchFilters + midiFilters + + vstPluginFilters + audioFilters; + + return extensions.join(" "); +} + } // namespace lmms::gui diff --git a/src/gui/FileBrowserSearcher.cpp b/src/gui/FileBrowserSearcher.cpp new file mode 100644 index 00000000000..80c2380580d --- /dev/null +++ b/src/gui/FileBrowserSearcher.cpp @@ -0,0 +1,135 @@ +/* + * FileBrowserSearcher.cpp - Batch processor for searching the filesystem + * + * Copyright (c) 2023 saker + * + * 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 "FileBrowserSearcher.h" + +#include +#include + +#include "FileBrowser.h" + +namespace lmms::gui { + +FileBrowserSearcher::~FileBrowserSearcher() +{ + m_cancelRunningSearch = true; + + { + const auto lock = std::lock_guard{m_workerMutex}; + m_workerStopped = true; + } + + m_workerCond.notify_one(); + m_worker.join(); +} + +auto FileBrowserSearcher::search(const QString& filter, const QStringList& paths, const QStringList& extensions) + -> std::shared_ptr +{ + m_cancelRunningSearch = true; + auto future = std::make_shared(filter, paths, extensions); + + { + const auto lock = std::lock_guard{m_workerMutex}; + m_searchQueue.push(future); + m_cancelRunningSearch = false; + } + + m_workerCond.notify_one(); + return future; +} + +auto FileBrowserSearcher::run() -> void +{ + while (true) + { + auto lock = std::unique_lock{m_workerMutex}; + m_workerCond.wait(lock, [this] { return m_workerStopped || !m_searchQueue.empty(); }); + + if (m_workerStopped) { return; } + + const auto future = m_searchQueue.front(); + future->m_state = SearchFuture::State::Running; + m_searchQueue.pop(); + + auto cancelled = false; + for (const auto& path : future->m_paths) + { + if (FileBrowser::directoryBlacklist().contains(path)) { continue; } + + if (!process(future.get(), path)) + { + future->m_state = SearchFuture::State::Cancelled; + cancelled = true; + break; + } + } + + if (!cancelled) { future->m_state = SearchFuture::State::Completed; } + } +} + +auto FileBrowserSearcher::process(SearchFuture* searchFuture, const QString& path) -> bool +{ + auto stack = QFileInfoList{}; + + auto dir = QDir{path}; + stack.append(dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags())); + + while (!stack.empty()) + { + if (m_cancelRunningSearch) + { + m_cancelRunningSearch = false; + return false; + } + + const auto info = stack.takeFirst(); + const auto path = info.absoluteFilePath(); + if (FileBrowser::directoryBlacklist().contains(path)) { continue; } + + const auto name = info.fileName(); + const auto validFile = info.isFile() && searchFuture->m_extensions.contains(info.suffix(), Qt::CaseInsensitive); + const auto passesFilter = name.contains(searchFuture->m_filter, Qt::CaseInsensitive); + + // Only when a directory doesn't pass the filter should we search further + if (info.isDir() && !passesFilter) + { + dir.setPath(path); + auto entries = dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags()); + + // Reverse to maintain the sorting within this directory when popped + std::reverse(entries.begin(), entries.end()); + + for (const auto& entry : entries) + { + stack.push_front(entry); + } + } + else if ((validFile || info.isDir()) && passesFilter) { searchFuture->addMatch(path); } + } + return true; +} + +} // namespace lmms::gui diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 830a994c8c6..77268bb9b75 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -137,7 +137,8 @@ AutoLilvNode Lv2ViewProc::uri(const char *uriStr) -Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) +Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) : + m_helpWindowEventFilter(this) { auto grid = new QGridLayout(meAsWidget); @@ -172,7 +173,7 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) LILV_FOREACH(nodes, itr, props.get()) { const LilvNode* node = lilv_nodes_get(props.get(), itr); - auto infoLabel = new QLabel(lilv_node_as_string(node)); + auto infoLabel = new QLabel(QString(lilv_node_as_string(node)).trimmed() + "\n"); infoLabel->setWordWrap(true); infoLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); @@ -181,8 +182,9 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) btnBox->addWidget(m_helpButton); m_helpWindow = getGUI()->mainWindow()->addWindowedWidget(infoLabel); - m_helpWindow->setSizePolicy(QSizePolicy::Minimum, + m_helpWindow->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_helpWindow->installEventFilter(&m_helpWindowEventFilter); m_helpWindow->setAttribute(Qt::WA_DeleteOnClose, false); m_helpWindow->hide(); @@ -203,6 +205,7 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) Lv2ViewBase::~Lv2ViewBase() { + closeHelpWindow(); // TODO: hide UI if required } @@ -228,6 +231,14 @@ void Lv2ViewBase::toggleHelp(bool visible) +void Lv2ViewBase::closeHelpWindow() +{ + if (m_helpWindow) { m_helpWindow->close(); } +} + + + + void Lv2ViewBase::modelChanged(Lv2ControlBase *ctrlBase) { // reconnect models @@ -248,6 +259,32 @@ AutoLilvNode Lv2ViewBase::uri(const char *uriStr) } + + +void Lv2ViewBase::onHelpWindowClosed() +{ + m_helpButton->setChecked(true); +} + + + + +HelpWindowEventFilter::HelpWindowEventFilter(Lv2ViewBase* viewBase) : + m_viewBase(viewBase) {} + + + + +bool HelpWindowEventFilter::eventFilter(QObject* , QEvent* event) +{ + if (event->type() == QEvent::Close) { + m_viewBase->m_helpButton->setChecked(false); + return true; + } + return false; +} + + } // namespace lmms::gui #endif // LMMS_HAVE_LV2 diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 10805fe01c4..62ed84c7aac 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -112,34 +112,27 @@ MainWindow::MainWindow() : sideBar->appendTab( new FileBrowser( confMgr->userProjectsDir() + "*" + confMgr->factoryProjectsDir(), - "*.mmp *.mmpz *.xml *.mid", + "*.mmp *.mmpz *.xml *.mid *.mpt", tr( "My Projects" ), embed::getIconPixmap( "project_file" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, true, + splitter, false, confMgr->userProjectsDir(), confMgr->factoryProjectsDir())); - sideBar->appendTab( new FileBrowser( - confMgr->userSamplesDir() + "*" + - confMgr->factorySamplesDir(), - "*", tr( "My Samples" ), - embed::getIconPixmap( "sample_file" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, true, - confMgr->userSamplesDir(), - confMgr->factorySamplesDir())); + sideBar->appendTab( + new FileBrowser(confMgr->userSamplesDir() + "*" + confMgr->factorySamplesDir(), FileItem::defaultFilters(), + tr("My Samples"), embed::getIconPixmap("sample_file").transformed(QTransform().rotate(90)), splitter, false, + confMgr->userSamplesDir(), confMgr->factorySamplesDir())); sideBar->appendTab( new FileBrowser( confMgr->userPresetsDir() + "*" + confMgr->factoryPresetsDir(), "*.xpf *.cs.xml *.xiz *.lv2", tr( "My Presets" ), embed::getIconPixmap( "preset_file" ).transformed( QTransform().rotate( 90 ) ), - splitter , false, true, + splitter , false, confMgr->userPresetsDir(), confMgr->factoryPresetsDir())); - sideBar->appendTab( new FileBrowser( QDir::homePath(), "*", - tr( "My Home" ), - embed::getIconPixmap( "home" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, false ) ); - + sideBar->appendTab(new FileBrowser(QDir::homePath(), FileItem::defaultFilters(), tr("My Home"), + embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false)); QStringList root_paths; QString title = tr( "Root directory" ); @@ -161,9 +154,8 @@ MainWindow::MainWindow() : } #endif - sideBar->appendTab( new FileBrowser( root_paths.join( "*" ), "*", title, - embed::getIconPixmap( "computer" ).transformed( QTransform().rotate( 90 ) ), - splitter, dirs_as_items) ); + sideBar->appendTab(new FileBrowser(root_paths.join("*"), FileItem::defaultFilters(), title, + embed::getIconPixmap("computer").transformed(QTransform().rotate(90)), splitter, dirs_as_items)); m_workspace = new QMdiArea(splitter); @@ -570,13 +562,21 @@ void MainWindow::addSpacingToToolBar( int _size ) 7, _size ); } + + + SubWindow* MainWindow::addWindowedWidget(QWidget *w, Qt::WindowFlags windowFlags) { // wrap the widget in our own *custom* window that patches some errors in QMdiSubWindow auto win = new SubWindow(m_workspace->viewport(), windowFlags); win->setAttribute(Qt::WA_DeleteOnClose); win->setWidget(w); - if (w && w->sizeHint().isValid()) {win->resize(w->sizeHint());} + if (w && w->sizeHint().isValid()) { + auto titleBarHeight = win->titleBarHeight(); + auto frameWidth = win->frameWidth(); + QSize delta(2* frameWidth, titleBarHeight + frameWidth); + win->resize(delta + w->sizeHint()); + } m_workspace->addSubWindow(win); return win; } diff --git a/src/gui/MidiSetupWidget.cpp b/src/gui/MidiSetupWidget.cpp index 4f620fb0ed1..2385def02dd 100644 --- a/src/gui/MidiSetupWidget.cpp +++ b/src/gui/MidiSetupWidget.cpp @@ -24,7 +24,7 @@ #include "MidiSetupWidget.h" -#include +#include #include #include "ConfigManager.h" @@ -37,7 +37,7 @@ namespace lmms::gui MidiSetupWidget::MidiSetupWidget(const QString & caption, const QString & configSection, const QString & devName, QWidget * parent) : - TabWidget(TabWidget::tr("Settings for %1").arg(tr(caption.toUtf8())), parent), + QGroupBox(QGroupBox::tr("Settings for %1").arg(tr(caption.toUtf8())), parent), m_configSection(configSection), m_device(nullptr) { @@ -45,12 +45,11 @@ MidiSetupWidget::MidiSetupWidget(const QString & caption, const QString & config // to indicate that there is no editable device field if (!devName.isNull()) { + QFormLayout * form = new QFormLayout(this); + m_device = new QLineEdit(devName, this); - m_device->setGeometry(10, 20, 160, 20); - auto dev_lbl = new QLabel(tr("Device"), this); - dev_lbl->setFont(pointSize<7>(dev_lbl->font())); - dev_lbl->setGeometry(10, 40, 160, 10); + form->addRow(tr("Device"), m_device); } } diff --git a/src/gui/MixerLine.cpp b/src/gui/MixerLine.cpp index a90f13f833e..182e131d3b8 100644 --- a/src/gui/MixerLine.cpp +++ b/src/gui/MixerLine.cpp @@ -68,8 +68,6 @@ bool MixerLine::eventFilter( QObject *dist, QEvent *event ) } const int MixerLine::MixerLineHeight = 287; -QPixmap * MixerLine::s_sendBgArrow = nullptr; -QPixmap * MixerLine::s_receiveBgArrow = nullptr; MixerLine::MixerLine( QWidget * _parent, MixerView * _mv, int _channelIndex ) : QWidget( _parent ), @@ -82,15 +80,6 @@ MixerLine::MixerLine( QWidget * _parent, MixerView * _mv, int _channelIndex ) : m_strokeInnerInactive( 0, 0, 0 ), m_inRename( false ) { - if( !s_sendBgArrow ) - { - s_sendBgArrow = new QPixmap( embed::getIconPixmap( "send_bg_arrow", 29, 56 ) ); - } - if( !s_receiveBgArrow ) - { - s_receiveBgArrow = new QPixmap( embed::getIconPixmap( "receive_bg_arrow", 29, 56 ) ); - } - setFixedSize( 33, MixerLineHeight ); setAttribute( Qt::WA_OpaquePaintEvent, true ); setCursor( QCursor( embed::getIconPixmap( "hand" ), 3, 3 ) ); @@ -174,9 +163,9 @@ void MixerLine::drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isA int width = mixerLine->rect().width(); int height = mixerLine->rect().height(); - if( channel->m_hasColor && !muted ) + if (channel->color().has_value() && !muted) { - p->fillRect( mixerLine->rect(), channel->m_color.darker( isActive ? 120 : 150 ) ); + p->fillRect(mixerLine->rect(), channel->color()->darker(isActive ? 120 : 150)); } else { @@ -193,14 +182,10 @@ void MixerLine::drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isA p->drawRect( 0, 0, width-1, height-1 ); // draw the mixer send background - if( sendToThis ) - { - p->drawPixmap( 2, 0, 29, 56, *s_sendBgArrow ); - } - else if( receiveFromThis ) - { - p->drawPixmap( 2, 0, 29, 56, *s_receiveBgArrow ); - } + + static auto s_sendBgArrow = embed::getIconPixmap("send_bg_arrow", 29, 56); + static auto s_receiveBgArrow = embed::getIconPixmap("receive_bg_arrow", 29, 56); + p->drawPixmap(2, 0, 29, 56, sendToThis ? s_sendBgArrow : s_receiveBgArrow); } @@ -430,36 +415,34 @@ void MixerLine::setStrokeInnerInactive( const QColor & c ) m_strokeInnerInactive = c; } - // Ask user for a color, and set it as the mixer line color void MixerLine::selectColor() { - auto channel = Engine::mixer()->mixerChannel( m_channelIndex ); - auto new_color = ColorChooser(this).withPalette(ColorChooser::Palette::Mixer)->getColor(channel->m_color); - if(!new_color.isValid()) { return; } - channel->setColor (new_color); + const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + const auto newColor = ColorChooser{this} + .withPalette(ColorChooser::Palette::Mixer) + ->getColor(channel->color().value_or(backgroundActive().color())); + if (!newColor.isValid()) { return; } + channel->setColor(newColor); Engine::getSong()->setModified(); update(); } - // Disable the usage of color on this mixer line void MixerLine::resetColor() { - Engine::mixer()->mixerChannel( m_channelIndex )->m_hasColor = false; + Engine::mixer()->mixerChannel(m_channelIndex)->setColor(std::nullopt); Engine::getSong()->setModified(); update(); } - // Pick a random color from the mixer palette and set it as our color void MixerLine::randomizeColor() { - auto channel = Engine::mixer()->mixerChannel( m_channelIndex ); - channel->setColor (ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]); + const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + channel->setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[std::rand() % 48]); Engine::getSong()->setModified(); update(); } - } // namespace lmms::gui diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index dff19ca3eb7..018e72c2b20 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -464,7 +464,7 @@ bool MixerView::confirmRemoval(int index) QString messageTitleRemoveTrack = tr("Confirm removal"); QString askAgainText = tr("Don't ask again"); auto askAgainCheckBox = new QCheckBox(askAgainText, nullptr); - connect(askAgainCheckBox, &QCheckBox::stateChanged, [this](int state) { + connect(askAgainCheckBox, &QCheckBox::stateChanged, [](int state) { // Invert button state, if it's checked we *shouldn't* ask again ConfigManager::inst()->setValue("ui", "mixerchanneldeletionwarning", state ? "0" : "1"); }); diff --git a/src/gui/PluginBrowser.cpp b/src/gui/PluginBrowser.cpp index 7ba8bcc5362..963609c431c 100644 --- a/src/gui/PluginBrowser.cpp +++ b/src/gui/PluginBrowser.cpp @@ -68,9 +68,10 @@ PluginBrowser::PluginBrowser( QWidget * _parent ) : hint->setWordWrap( true ); auto searchBar = new QLineEdit(m_view); - searchBar->setPlaceholderText( "Search" ); - searchBar->setMaxLength( 64 ); - searchBar->setClearButtonEnabled( true ); + searchBar->setPlaceholderText(tr("Search")); + searchBar->setMaxLength(64); + searchBar->setClearButtonEnabled(true); + searchBar->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); m_descTree = new QTreeWidget( m_view ); m_descTree->setColumnCount( 1 ); @@ -281,9 +282,9 @@ void PluginDescWidget::leaveEvent( QEvent * _e ) void PluginDescWidget::mousePressEvent( QMouseEvent * _me ) { + Engine::setDndPluginKey(&m_pluginKey); if ( _me->button() == Qt::LeftButton ) { - Engine::setDndPluginKey(&m_pluginKey); new StringPairDrag("instrument", QString::fromUtf8(m_pluginKey.desc->name), m_logo, this); leaveEvent( _me ); diff --git a/src/gui/SendButtonIndicator.cpp b/src/gui/SendButtonIndicator.cpp index cd1996c4564..d6f8a832778 100644 --- a/src/gui/SendButtonIndicator.cpp +++ b/src/gui/SendButtonIndicator.cpp @@ -8,30 +8,17 @@ namespace lmms::gui { - -QPixmap * SendButtonIndicator::s_qpmOff = nullptr; -QPixmap * SendButtonIndicator::s_qpmOn = nullptr; - SendButtonIndicator:: SendButtonIndicator( QWidget * _parent, MixerLine * _owner, MixerView * _mv) : QLabel( _parent ), m_parent( _owner ), m_mv( _mv ) { - if( ! s_qpmOff ) - { - s_qpmOff = new QPixmap( embed::getIconPixmap( "mixer_send_off", 29, 20 ) ); - } - - if( ! s_qpmOn ) - { - s_qpmOn = new QPixmap( embed::getIconPixmap( "mixer_send_on", 29, 20 ) ); - } - + // don't do any initializing yet, because the MixerView and MixerLine // that were passed to this constructor are not done with their constructors // yet. - setPixmap( *s_qpmOff ); + setPixmap(m_qpmOff); } void SendButtonIndicator::mousePressEvent( QMouseEvent * e ) @@ -64,7 +51,7 @@ FloatModel * SendButtonIndicator::getSendModel() void SendButtonIndicator::updateLightStatus() { - setPixmap( getSendModel() == nullptr ? *s_qpmOff : *s_qpmOn ); + setPixmap(!getSendModel() ? m_qpmOff : m_qpmOn); } diff --git a/src/gui/SubWindow.cpp b/src/gui/SubWindow.cpp index 78e4f586c67..dc6e49297d1 100644 --- a/src/gui/SubWindow.cpp +++ b/src/gui/SubWindow.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "embed.h" @@ -41,11 +42,11 @@ namespace lmms::gui { -SubWindow::SubWindow( QWidget *parent, Qt::WindowFlags windowFlags ) : - QMdiSubWindow( parent, windowFlags ), - m_buttonSize( 17, 17 ), - m_titleBarHeight( 24 ), - m_hasFocus( false ) +SubWindow::SubWindow(QWidget *parent, Qt::WindowFlags windowFlags) : + QMdiSubWindow(parent, windowFlags), + m_buttonSize(17, 17), + m_titleBarHeight(titleBarHeight()), + m_hasFocus(false) { // initialize the tracked geometry to whatever Qt thinks the normal geometry currently is. // this should always work, since QMdiSubWindows will not start as maximized @@ -240,6 +241,27 @@ void SubWindow::setBorderColor( const QColor &c ) + +int SubWindow::titleBarHeight() const +{ + QStyleOptionTitleBar so; + so.titleBarState = Qt::WindowActive; // kThemeStateActiv + so.titleBarFlags = Qt::Window; + return style()->pixelMetric(QStyle::PM_TitleBarHeight, &so, this); +} + + + + +int SubWindow::frameWidth() const +{ + QStyleOptionFrame so; + return style()->pixelMetric(QStyle::PM_MdiSubWindowFrameWidth, &so, this); +} + + + + /** * @brief SubWindow::moveEvent * diff --git a/src/gui/clips/AutomationClipView.cpp b/src/gui/clips/AutomationClipView.cpp index 3e0e12b75d0..7ddb7015182 100644 --- a/src/gui/clips/AutomationClipView.cpp +++ b/src/gui/clips/AutomationClipView.cpp @@ -44,8 +44,6 @@ namespace lmms::gui { -QPixmap * AutomationClipView::s_clip_rec = nullptr; - AutomationClipView::AutomationClipView( AutomationClip * _clip, TrackView * _parent ) : ClipView( _clip, _parent ), @@ -61,10 +59,6 @@ AutomationClipView::AutomationClipView( AutomationClip * _clip, setToolTip(m_clip->name()); setStyle( QApplication::style() ); - - if( s_clip_rec == nullptr ) { s_clip_rec = new QPixmap( embed::getIconPixmap( - "clip_rec" ) ); } - update(); } @@ -379,7 +373,8 @@ void AutomationClipView::paintEvent( QPaintEvent * ) // recording icon for when recording automation if( m_clip->isRecording() ) { - p.drawPixmap( 1, rect().bottom() - s_clip_rec->height(), *s_clip_rec ); + static auto s_clipRec = embed::getIconPixmap("clip_rec"); + p.drawPixmap(1, rect().bottom() - s_clipRec.height(), s_clipRec); } // clip name diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index de7690d26e1..b2ad5c99cc7 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -136,7 +136,7 @@ ClipView::ClipView( Clip * clip, connect(m_trackView->getTrack(), &Track::colorChanged, this, [this] { // redraw if clip uses track color - if (!m_clip->usesCustomClipColor()) { update(); } + if (!m_clip->color().has_value()) { update(); } }); m_trackView->getTrackContentWidget()->addClipView( this ); @@ -340,45 +340,35 @@ void ClipView::updatePosition() m_trackView->trackContainerView()->update(); } - - - void ClipView::selectColor() { // Get a color from the user - QColor new_color = ColorChooser( this ).withPalette( ColorChooser::Palette::Track )->getColor( m_clip->color() ); - if (new_color.isValid()) { setColor(&new_color); } + const auto newColor = ColorChooser{this} + .withPalette(ColorChooser::Palette::Track) + ->getColor(m_clip->color().value_or(palette().background().color())); + if (newColor.isValid()) { setColor(newColor); } } - - - void ClipView::randomizeColor() { - setColor(&ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]); + setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[std::rand() % 48]); } - - - void ClipView::resetColor() { - setColor(nullptr); + setColor(std::nullopt); } - - - /*! \brief Change color of all selected clips * - * \param color The new QColor. Pass nullptr to use the Track's color. + * \param color The new color. */ -void ClipView::setColor(const QColor* color) +void ClipView::setColor(const std::optional& color) { std::set journaledTracks; auto selectedClips = getClickedClips(); - for (auto clipv: selectedClips) + for (auto clipv : selectedClips) { auto clip = clipv->getClip(); auto track = clip->getTrack(); @@ -397,25 +387,13 @@ void ClipView::setColor(const QColor* color) track->addJournalCheckPoint(); } - if (color) - { - clip->useCustomClipColor(true); - clip->setColor(*color); - } - else - { - clip->useCustomClipColor(false); - } + clip->setColor(color); clipv->update(); } Engine::getSong()->setModified(); } - - - - /*! \brief Change the ClipView's display when something * being dragged enters it. * @@ -1483,11 +1461,7 @@ TimePos ClipView::quantizeSplitPos( TimePos midiPos, bool shiftMode ) QColor ClipView::getColorForDisplay( QColor defaultColor ) { // Get the pure Clip color - auto clipColor = m_clip->hasColor() - ? m_clip->usesCustomClipColor() - ? m_clip->color() - : m_clip->getTrack()->color() - : defaultColor; + auto clipColor = m_clip->color().value_or(m_clip->getTrack()->color().value_or(defaultColor)); // Set variables QColor c, mutedCustomColor; @@ -1498,7 +1472,7 @@ QColor ClipView::getColorForDisplay( QColor defaultColor ) // Change the pure color by state: selected, muted, colored, normal if( isSelected() ) { - c = m_clip->hasColor() + c = hasCustomColor() ? ( muted ? mutedCustomColor.darker( 350 ) : clipColor.darker( 150 ) ) @@ -1508,7 +1482,7 @@ QColor ClipView::getColorForDisplay( QColor defaultColor ) { if( muted ) { - c = m_clip->hasColor() + c = hasCustomColor() ? mutedCustomColor.darker( 250 ) : mutedBackgroundColor(); } @@ -1522,5 +1496,9 @@ QColor ClipView::getColorForDisplay( QColor defaultColor ) return c; } +auto ClipView::hasCustomColor() const -> bool +{ + return m_clip->color().has_value() || m_clip->getTrack()->color().has_value(); +} } // namespace lmms::gui diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 151df8d3c3c..a9ee1cb4008 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -25,12 +25,16 @@ #include "MidiClipView.h" + +#include #include #include #include #include #include +#include +#include "AutomationEditor.h" #include "ConfigManager.h" #include "DeprecationHelper.h" #include "GuiApplication.h" @@ -56,31 +60,6 @@ MidiClipView::MidiClipView( MidiClip* clip, TrackView* parent ) : { connect( getGUI()->pianoRoll(), SIGNAL(currentMidiClipChanged()), this, SLOT(update())); - - if( s_stepBtnOn0 == nullptr ) - { - s_stepBtnOn0 = new QPixmap( embed::getIconPixmap( - "step_btn_on_0" ) ); - } - - if( s_stepBtnOn200 == nullptr ) - { - s_stepBtnOn200 = new QPixmap( embed::getIconPixmap( - "step_btn_on_200" ) ); - } - - if( s_stepBtnOff == nullptr ) - { - s_stepBtnOff = new QPixmap( embed::getIconPixmap( - "step_btn_off" ) ); - } - - if( s_stepBtnOffLight == nullptr ) - { - s_stepBtnOffLight = new QPixmap( embed::getIconPixmap( - "step_btn_off_light" ) ); - } - update(); setStyle( QApplication::style() ); @@ -109,10 +88,11 @@ void MidiClipView::update() void MidiClipView::openInPianoRoll() { - getGUI()->pianoRoll()->setCurrentMidiClip( m_clip ); - getGUI()->pianoRoll()->parentWidget()->show(); - getGUI()->pianoRoll()->show(); - getGUI()->pianoRoll()->setFocus(); + auto pRoll = getGUI()->pianoRoll(); + pRoll->setCurrentMidiClip(m_clip); + pRoll->parentWidget()->show(); + pRoll->show(); + pRoll->setFocus(); } @@ -121,14 +101,21 @@ void MidiClipView::openInPianoRoll() void MidiClipView::setGhostInPianoRoll() { - getGUI()->pianoRoll()->setGhostMidiClip( m_clip ); - getGUI()->pianoRoll()->parentWidget()->show(); - getGUI()->pianoRoll()->show(); - getGUI()->pianoRoll()->setFocus(); + auto pRoll = getGUI()->pianoRoll(); + pRoll->setGhostMidiClip(m_clip); + pRoll->parentWidget()->show(); + pRoll->show(); + pRoll->setFocus(); } - - +void MidiClipView::setGhostInAutomationEditor() +{ + auto aEditor = getGUI()->automationEditor(); + aEditor->setGhostMidiClip(m_clip); + aEditor->parentWidget()->show(); + aEditor->show(); + aEditor->setFocus(); +} void MidiClipView::resetName() { m_clip->setName(""); } @@ -216,7 +203,13 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) _cm->insertAction( _cm->actions()[1], b ); connect( b, SIGNAL(triggered(bool)), this, SLOT(setGhostInPianoRoll())); - _cm->insertSeparator( _cm->actions()[2] ); + + auto c = new QAction(embed::getIconPixmap("automation_ghost_note"), tr("Set as ghost in automation editor"), _cm); + if (m_clip->empty()) { c->setEnabled(false); } + _cm->insertAction(_cm->actions()[2], c); + connect(c, &QAction::triggered, this, &MidiClipView::setGhostInAutomationEditor); + + _cm->insertSeparator(_cm->actions()[3]); _cm->addSeparator(); _cm->addAction( embed::getIconPixmap( "edit_erase" ), @@ -252,9 +245,8 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) void MidiClipView::mousePressEvent( QMouseEvent * _me ) { bool displayPattern = fixedClips() || (pixelsPerBar() >= 96 && m_legacySEPattern); - if( _me->button() == Qt::LeftButton && - m_clip->m_clipType == MidiClip::Type::BeatClip && - displayPattern && _me->y() > height() - s_stepBtnOff->height() ) + if (_me->button() == Qt::LeftButton && m_clip->m_clipType == MidiClip::Type::BeatClip && displayPattern + && _me->y() > height() - m_stepBtnOff.height()) // when mouse button is pressed in pattern mode @@ -324,7 +316,7 @@ void MidiClipView::wheelEvent(QWheelEvent * we) { if(m_clip->m_clipType == MidiClip::Type::BeatClip && (fixedClips() || pixelsPerBar() >= 96) && - position(we).y() > height() - s_stepBtnOff->height()) + position(we).y() > height() - m_stepBtnOff.height()) { // get the step number that was wheeled on and // do calculations in floats to prevent rounding errors... @@ -458,9 +450,70 @@ void MidiClipView::paintEvent( QPaintEvent * ) const int x_base = BORDER_WIDTH; bool displayPattern = fixedClips() || (pixelsPerBar >= 96 && m_legacySEPattern); - // melody clip paint event NoteVector const & noteCollection = m_clip->m_notes; - if( m_clip->m_clipType == MidiClip::Type::MelodyClip && !noteCollection.empty() ) + + // Beat clip paint event (on BB Editor) + if (beatClip && displayPattern) + { + QPixmap stepon0; + QPixmap stepon200; + QPixmap stepoff; + QPixmap stepoffl; + const int steps = std::max(1, m_clip->m_steps); + const int w = width() - 2 * BORDER_WIDTH; + + // scale step graphics to fit the beat clip length + stepon0 + = m_stepBtnOn0.scaled(w / steps, m_stepBtnOn0.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + stepon200 = m_stepBtnOn200.scaled( + w / steps, m_stepBtnOn200.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + stepoff + = m_stepBtnOff.scaled(w / steps, m_stepBtnOff.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + stepoffl = m_stepBtnOffLight.scaled( + w / steps, m_stepBtnOffLight.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + + for (int it = 0; it < steps; it++) // go through all the steps in the beat clip + { + Note* n = m_clip->noteAtStep(it); + + // figure out x and y coordinates for step graphic + const int x = BORDER_WIDTH + static_cast(it * w / steps); + const int y = height() - m_stepBtnOff.height() - 1; + + if (n) + { + const int vol = n->getVolume(); + p.drawPixmap(x, y, stepoffl); + p.drawPixmap(x, y, stepon0); + p.setOpacity(std::sqrt(vol / 200.0)); + p.drawPixmap(x, y, stepon200); + p.setOpacity(1); + } + else if ((it / 4) % 2) + { + p.drawPixmap(x, y, stepoffl); + } + else + { + p.drawPixmap(x, y, stepoff); + } + } // end for loop + + // draw a transparent rectangle over muted clips + if (muted) + { + p.setBrush(mutedBackgroundColor()); + p.setOpacity(0.5); + p.drawRect(0, 0, width(), height()); + } + } + // Melody clip and Beat clip (on Song Editor) paint event + else if + ( + !noteCollection.empty() && + (m_clip->m_clipType == MidiClip::Type::MelodyClip || + m_clip->m_clipType == MidiClip::Type::BeatClip) + ) { // Compute the minimum and maximum key in the clip // so that we know how much there is to draw. @@ -526,7 +579,7 @@ void MidiClipView::paintEvent( QPaintEvent * ) QColor noteFillColor = muted ? getMutedNoteFillColor().lighter(200) : (c.lightness() > 175 ? getNoteFillColor().darker(400) : getNoteFillColor()); QColor noteBorderColor = muted ? getMutedNoteBorderColor() - : ( m_clip->hasColor() ? c.lighter( 200 ) : getNoteBorderColor() ); + : (hasCustomColor() ? c.lighter(200) : getNoteBorderColor()); bool const drawAsLines = height() < 64; if (drawAsLines) @@ -574,70 +627,6 @@ void MidiClipView::paintEvent( QPaintEvent * ) p.restore(); } - // beat clip paint event - else if (beatClip && displayPattern) - { - QPixmap stepon0; - QPixmap stepon200; - QPixmap stepoff; - QPixmap stepoffl; - const int steps = qMax( 1, - m_clip->m_steps ); - const int w = width() - 2 * BORDER_WIDTH; - - // scale step graphics to fit the beat clip length - stepon0 = s_stepBtnOn0->scaled( w / steps, - s_stepBtnOn0->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - stepon200 = s_stepBtnOn200->scaled( w / steps, - s_stepBtnOn200->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - stepoff = s_stepBtnOff->scaled( w / steps, - s_stepBtnOff->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - stepoffl = s_stepBtnOffLight->scaled( w / steps, - s_stepBtnOffLight->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - - for( int it = 0; it < steps; it++ ) // go through all the steps in the beat clip - { - Note * n = m_clip->noteAtStep( it ); - - // figure out x and y coordinates for step graphic - const int x = BORDER_WIDTH + static_cast( it * w / steps ); - const int y = height() - s_stepBtnOff->height() - 1; - - if( n ) - { - const int vol = n->getVolume(); - p.drawPixmap( x, y, stepoffl ); - p.drawPixmap( x, y, stepon0 ); - p.setOpacity( sqrt( vol / 200.0 ) ); - p.drawPixmap( x, y, stepon200 ); - p.setOpacity( 1 ); - } - else if( ( it / 4 ) % 2 ) - { - p.drawPixmap( x, y, stepoffl ); - } - else - { - p.drawPixmap( x, y, stepoff ); - } - } // end for loop - - // draw a transparent rectangle over muted clips - if ( muted ) - { - p.setBrush( mutedBackgroundColor() ); - p.setOpacity( 0.5 ); - p.drawRect( 0, 0, width(), height() ); - } - } // bar lines const int lineSize = 3; diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index e21a7e30be1..81bbd271d54 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -28,6 +28,8 @@ #include #include +#include "GuiApplication.h" +#include "AutomationEditor.h" #include "embed.h" #include "PathUtil.h" #include "SampleBuffer.h" @@ -83,6 +85,12 @@ void SampleClipView::constructContextMenu(QMenu* cm) SLOT(reverseSample()) ); + cm->addAction( + embed::getIconPixmap("automation_ghost_note"), + tr("Set as ghost in automation editor"), + this, + SLOT(setAutomationGhost()) + ); } @@ -231,11 +239,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) p.fillRect( rect(), c ); } - auto clipColor = m_clip->hasColor() - ? (m_clip->usesCustomClipColor() - ? m_clip->color() - : m_clip->getTrack()->color()) - : painter.pen().brush().color(); + auto clipColor = m_clip->color().value_or(m_clip->getTrack()->color().value_or(painter.pen().brush().color())); p.setPen(clipColor); @@ -325,6 +329,14 @@ void SampleClipView::reverseSample() +void SampleClipView::setAutomationGhost() +{ + auto aEditor = gui::getGUI()->automationEditor(); + aEditor->setGhostSample(m_clip); + aEditor->parentWidget()->show(); + aEditor->show(); + aEditor->setFocus(); +} //! Split this Clip. /*! \param pos the position of the split, relative to the start of the clip */ diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index f98066bbaa2..c8ef19b79bd 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -27,8 +27,6 @@ #include "AutomationEditor.h" -#include - #include #include #include @@ -38,6 +36,9 @@ #include #include #include +#include + +#include "SampleClip.h" #ifndef __USE_XOPEN #define __USE_XOPEN @@ -46,32 +47,27 @@ #include "ActionGroup.h" #include "AutomationNode.h" #include "ComboBox.h" -#include "debug.h" #include "DeprecationHelper.h" -#include "embed.h" +#include "DetuningHelper.h" #include "Engine.h" #include "GuiApplication.h" -#include "gui_templates.h" #include "Knob.h" #include "MainWindow.h" +#include "MidiClip.h" #include "PatternStore.h" #include "PianoRoll.h" #include "ProjectJournal.h" +#include "SampleBuffer.h" #include "StringPairDrag.h" #include "TextFloat.h" #include "TimeLineWidget.h" +#include "debug.h" +#include "embed.h" +#include "gui_templates.h" namespace lmms::gui { - -QPixmap * AutomationEditor::s_toolDraw = nullptr; -QPixmap * AutomationEditor::s_toolErase = nullptr; -QPixmap * AutomationEditor::s_toolDrawOut = nullptr; -QPixmap * AutomationEditor::s_toolMove = nullptr; -QPixmap * AutomationEditor::s_toolYFlip = nullptr; -QPixmap * AutomationEditor::s_toolXFlip = nullptr; - const std::array AutomationEditor::m_zoomXLevels = { 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f }; @@ -106,9 +102,11 @@ AutomationEditor::AutomationEditor() : m_graphColor(Qt::SolidPattern), m_nodeInValueColor(0, 0, 0), m_nodeOutValueColor(0, 0, 0), + m_nodeTangentLineColor(0, 0, 0), m_scaleColor(Qt::SolidPattern), m_crossColor(0, 0, 0), - m_backgroundShade(0, 0, 0) + m_backgroundShade(0, 0, 0), + m_ghostNoteColor(0, 0, 0) { connect( this, SIGNAL(currentClipChanged()), this, SLOT(updateAfterClipChange()), @@ -133,17 +131,6 @@ AutomationEditor::AutomationEditor() : this, SLOT(setQuantization())); m_quantizeModel.setValue( m_quantizeModel.findText( "1/8" ) ); - if (s_toolYFlip == nullptr) - { - s_toolYFlip = new QPixmap( embed::getIconPixmap( - "flip_y" ) ); - } - if (s_toolXFlip == nullptr) - { - s_toolXFlip = new QPixmap( embed::getIconPixmap( - "flip_x" ) ); - } - // add time-line m_timeLine = new TimeLineWidget( VALUES_WIDTH, 0, m_ppb, Engine::getSong()->getPlayPos( @@ -167,24 +154,6 @@ AutomationEditor::AutomationEditor() : connect( m_topBottomScroll, SIGNAL(valueChanged(int)), this, SLOT(verScrolled(int))); - // init pixmaps - if (s_toolDraw == nullptr) - { - s_toolDraw = new QPixmap(embed::getIconPixmap("edit_draw")); - } - if (s_toolErase == nullptr) - { - s_toolErase= new QPixmap(embed::getIconPixmap("edit_erase")); - } - if (s_toolDrawOut == nullptr) - { - s_toolDrawOut = new QPixmap(embed::getIconPixmap("edit_draw_outvalue")); - } - if (s_toolMove == nullptr) - { - s_toolMove = new QPixmap(embed::getIconPixmap("edit_move")); - } - setCurrentClip(nullptr); setMouseTracking( true ); @@ -470,6 +439,17 @@ void AutomationEditor::mousePressEvent( QMouseEvent* mouseEvent ) Engine::getSong()->setModified(); } }; + auto resetTangent = [this](timeMap::iterator node) + { + if (node != m_clip->getTimeMap().end()) + { + // Unlock the tangents from that node + node.value().setLockedTangents(false); + // Recalculate the tangents + m_clip->generateTangents(node, 1); + Engine::getSong()->setModified(); + } + }; // If we clicked inside the AutomationEditor viewport (where the nodes are represented) if (mouseEvent->y() > TOP_MARGIN && mouseEvent->x() >= VALUES_WIDTH) @@ -654,6 +634,47 @@ void AutomationEditor::mousePressEvent( QMouseEvent* mouseEvent ) } break; } + case EditMode::EditTangents: + { + if (!m_clip->canEditTangents()) + { + update(); + return; + } + + m_clip->addJournalCheckPoint(); + + // Gets the closest node to the mouse click + timeMap::iterator node = getClosestNode(mouseEvent->x()); + + // Starts dragging a tangent + if (m_mouseDownLeft && node != tm.end()) + { + // Lock the tangents from that node, so it can only be + // manually edited + node.value().setLockedTangents(true); + + m_draggedTangentTick = POS(node); + + // Are we dragging the out or in tangent? + m_draggedOutTangent = posTicks >= m_draggedTangentTick; + + m_action = Action::MoveTangent; + } + // Resets node's tangent + else if (m_mouseDownRight) + { + // Resets tangent from node + resetTangent(node); + + // Update the last clicked position so we reset all tangents from + // that point up to the point we release the mouse button + m_drawLastTick = posTicks; + + m_action = Action::ResetTangents; + } + break; + } } update(); @@ -862,6 +883,51 @@ void AutomationEditor::mouseMoveEvent(QMouseEvent * mouseEvent ) } break; } + case EditMode::EditTangents: + { + // If we moved the mouse past the beginning correct the position in ticks + posTicks = std::max(posTicks, 0); + + if (m_mouseDownLeft && m_action == Action::MoveTangent) + { + timeMap& tm = m_clip->getTimeMap(); + auto it = tm.find(m_draggedTangentTick); + + // Safety check + if (it == tm.end()) + { + update(); + return; + } + + // Calculate new tangent + float y = m_draggedOutTangent + ? yCoordOfLevel(OUTVAL(it)) + : yCoordOfLevel(INVAL(it)); + float dy = m_draggedOutTangent + ? y - mouseEvent->y() + : mouseEvent->y() - y; + float dx = std::abs(posTicks - POS(it)); + float newTangent = dy / std::max(dx, 1.0f); + + if (m_draggedOutTangent) + { + it.value().setOutTangent(newTangent); + } + else + { + it.value().setInTangent(newTangent); + } + } + else if (m_mouseDownRight && m_action == Action::ResetTangents) + { + // Resets all tangents from the last clicked tick up to the current position tick + m_clip->resetTangents(m_drawLastTick, posTicks); + + Engine::getSong()->setModified(); + } + break; + } } } else // If the mouse Y position is above the AutomationEditor viewport @@ -937,6 +1003,54 @@ inline void AutomationEditor::drawAutomationPoint(QPainter & p, timeMap::iterato +inline void AutomationEditor::drawAutomationTangents(QPainter& p, timeMap::iterator it) +{ + int x = xCoordOfTick(POS(it)); + int y, tx, ty; + + // The tangent value correlates the variation in the node value related to the increase + // in ticks. So to have a proportionate drawing of the tangent line, we need to find the + // relation between the number of pixels per tick and the number of pixels per value level. + float viewportHeight = (height() - SCROLLBAR_SIZE - 1) - TOP_MARGIN; + float pixelsPerTick = m_ppb / TimePos::ticksPerBar(); + // std::abs just in case the topLevel is smaller than the bottomLevel for some reason + float pixelsPerLevel = std::abs(viewportHeight / (m_topLevel - m_bottomLevel)); + float proportion = pixelsPerLevel / pixelsPerTick; + + p.setPen(QPen(m_nodeTangentLineColor)); + p.setBrush(QBrush(m_nodeTangentLineColor)); + + y = yCoordOfLevel(INVAL(it)); + tx = x - 20; + ty = y + 20 * INTAN(it) * proportion; + p.drawLine(x, y, tx, ty); + p.setBrush(QBrush(m_nodeTangentLineColor.darker(200))); + p.drawEllipse(tx - 3, ty - 3, 6, 6); + + p.setBrush(QBrush(m_nodeTangentLineColor)); + + y = yCoordOfLevel(OUTVAL(it)); + tx = x + 20; + ty = y - 20 * OUTTAN(it) * proportion; + p.drawLine(x, y, tx, ty); + p.setBrush(QBrush(m_nodeTangentLineColor.darker(200))); + p.drawEllipse(tx - 3, ty - 3, 6, 6); +} + +void AutomationEditor::setGhostMidiClip(MidiClip* newMidiClip) +{ + // Expects a pointer to a MIDI clip or nullptr. + m_ghostNotes = newMidiClip; + m_renderSample = false; +} + +void AutomationEditor::setGhostSample(SampleClip* newGhostSample) +{ + // Expects a pointer to a Sample buffer or nullptr. + m_ghostSample = newGhostSample; + m_renderSample = true; +} + void AutomationEditor::paintEvent(QPaintEvent * pe ) { QStyleOption opt; @@ -1121,6 +1235,81 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) p.drawLine( x, grid_bottom, x, x_line_end ); } + // draw ghost sample + if (m_ghostSample != nullptr && m_ghostSample->sampleBuffer()->frames() > 1 && m_renderSample) + { + int sampleFrames = m_ghostSample->sampleBuffer()->frames(); + int length = static_cast(sampleFrames) / Engine::framesPerTick(); + int editorHeight = grid_bottom - TOP_MARGIN; + + int startPos = xCoordOfTick(0); + int sampleWidth = xCoordOfTick(length) - startPos; + int sampleHeight = std::min(editorHeight - SAMPLE_MARGIN, MAX_SAMPLE_HEIGHT); + int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN; + + p.setPen(m_ghostSampleColor); + m_ghostSample->sampleBuffer()->visualize(p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); + } + + // draw ghost notes + if (m_ghostNotes != nullptr && !m_renderSample) + { + const NoteVector& notes = m_ghostNotes->notes(); + int minKey = 128; + int maxKey = 0; + int detuningOffset = 0; + const Note* detuningNote = nullptr; + + for (const Note* note : notes) + { + int noteKey = note->key(); + + if (note->detuning()->automationClip() == m_clip) { + detuningOffset = note->pos(); + detuningNote = note; + } + + maxKey = std::max(maxKey, noteKey); + minKey = std::min(minKey, noteKey); + } + + for (const Note* note : notes) + { + int lenTicks = note->length(); + int notePos = note->pos(); + + // offset note if detuning + if (notePos+lenTicks < detuningOffset) { continue; } + notePos -= detuningOffset; + + // remove/change after #5902 + if (lenTicks == 0) { continue; } + else if (lenTicks < 0) { lenTicks = 4; } + + int note_width = lenTicks * m_ppb / TimePos::ticksPerBar(); + int keyRange = maxKey - minKey; + + if (keyRange < MIN_NOTE_RANGE) + { + int padding = (MIN_NOTE_RANGE - keyRange) / 2.0f; + maxKey += padding; + minKey -= padding; + keyRange = MIN_NOTE_RANGE; + } + + float absNoteHeight = static_cast(note->key() - minKey) / (maxKey - minKey); + int graphHeight = grid_bottom - NOTE_HEIGHT - NOTE_MARGIN - TOP_MARGIN; + const int y = (graphHeight - graphHeight * absNoteHeight) + NOTE_HEIGHT / 2.0f + TOP_MARGIN; + const int x = xCoordOfTick(notePos); + + if (note == detuningNote) { + p.fillRect(x, y, note_width, NOTE_HEIGHT, m_detuningNoteColor); + } else { + p.fillRect(x, y, note_width, NOTE_HEIGHT, m_ghostNoteColor); + } + } + } + // and finally bars for( tick = m_currentPosition - m_currentPosition % TimePos::ticksPerBar(), x = xCoordOfTick( tick ); @@ -1197,6 +1386,11 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) // Draw circle drawAutomationPoint(p, it); + // Draw tangents if necessary (only for manually edited tangents) + if (m_clip->canEditTangents() && LOCKEDTAN(it)) + { + drawAutomationTangents(p, it); + } ++it; } @@ -1213,6 +1407,11 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } // Draw circle(the last one) drawAutomationPoint(p, it); + // Draw tangents if necessary (only for manually edited tangents) + if (m_clip->canEditTangents() && LOCKEDTAN(it)) + { + drawAutomationTangents(p, it); + } } } else @@ -1250,21 +1449,26 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) { case EditMode::Draw: { - if (m_action == Action::EraseValues) { cursor = s_toolErase; } - else if (m_action == Action::MoveValue) { cursor = s_toolMove; } - else { cursor = s_toolDraw; } + if (m_action == Action::EraseValues) { cursor = &m_toolErase; } + else if (m_action == Action::MoveValue) { cursor = &m_toolMove; } + else { cursor = &m_toolDraw; } break; } case EditMode::Erase: { - cursor = s_toolErase; + cursor = &m_toolErase; break; } case EditMode::DrawOutValues: { - if (m_action == Action::ResetOutValues) { cursor = s_toolErase; } - else if (m_action == Action::MoveOutValue) { cursor = s_toolMove; } - else { cursor = s_toolDrawOut; } + if (m_action == Action::ResetOutValues) { cursor = &m_toolErase; } + else if (m_action == Action::MoveOutValue) { cursor = &m_toolMove; } + else { cursor = &m_toolDrawOut; } + break; + } + case EditMode::EditTangents: + { + cursor = m_action == Action::MoveTangent ? &m_toolMove : &m_toolEditTangents; break; } } @@ -1819,6 +2023,49 @@ AutomationEditor::timeMap::iterator AutomationEditor::getNodeAt(int x, int y, bo return tm.end(); } +AutomationEditor::timeMap::iterator AutomationEditor::getClosestNode(int x) +{ + // Remove the VALUES_WIDTH from the x position, so we have the actual viewport x + x -= VALUES_WIDTH; + // Convert the x position to the position in ticks + int posTicks = (x * TimePos::ticksPerBar() / m_ppb) + m_currentPosition; + + // Get our pattern timeMap and create a iterator so we can check the nodes + timeMap& tm = m_clip->getTimeMap(); + + if (tm.isEmpty()) { return tm.end(); } + + // Get the node with an equal or higher position + auto it = tm.lowerBound(posTicks); + + // If there are no nodes equal or higher than the position return + // the one before it + if (it == tm.end()) + { + --it; + return it; + } + // If the node returned is the first, return it + else if (it == tm.begin()) + { + return it; + } + // Else return the closest node + else + { + // Distance from node to the right + int distanceRight = std::abs(POS(it) - posTicks); + // Distance from node to the left + int distanceLeft = std::abs(POS(--it) - posTicks); + + if (distanceLeft >= distanceRight) + { + ++it; + } + return it; + } +} + @@ -1839,24 +2086,29 @@ AutomationEditorWindow::AutomationEditorWindow() : DropToolBar *editActionsToolBar = addDropToolBarToTop(tr("Edit actions")); auto editModeGroup = new ActionGroup(this); - QAction* drawAction = editModeGroup->addAction(embed::getIconPixmap("edit_draw"), tr("Draw mode (Shift+D)")); - drawAction->setShortcut(Qt::SHIFT | Qt::Key_D); - drawAction->setChecked(true); + m_drawAction = editModeGroup->addAction(embed::getIconPixmap("edit_draw"), tr("Draw mode (Shift+D)")); + m_drawAction->setShortcut(Qt::SHIFT | Qt::Key_D); + m_drawAction->setChecked(true); + + m_eraseAction = editModeGroup->addAction(embed::getIconPixmap("edit_erase"), tr("Erase mode (Shift+E)")); + m_eraseAction->setShortcut(Qt::SHIFT | Qt::Key_E); - QAction* eraseAction = editModeGroup->addAction(embed::getIconPixmap("edit_erase"), tr("Erase mode (Shift+E)")); - eraseAction->setShortcut(Qt::SHIFT | Qt::Key_E); + m_drawOutAction = editModeGroup->addAction(embed::getIconPixmap("edit_draw_outvalue"), tr("Draw outValues mode (Shift+C)")); + m_drawOutAction->setShortcut(Qt::SHIFT | Qt::Key_C); - QAction* drawOutAction = editModeGroup->addAction(embed::getIconPixmap("edit_draw_outvalue"), tr("Draw outValues mode (Shift+C)")); - drawOutAction->setShortcut(Qt::SHIFT | Qt::Key_C); + m_editTanAction = editModeGroup->addAction(embed::getIconPixmap("edit_tangent"), tr("Edit tangents mode (Shift+T)")); + m_editTanAction->setShortcut(Qt::SHIFT | Qt::Key_T); + m_editTanAction->setEnabled(false); m_flipYAction = new QAction(embed::getIconPixmap("flip_y"), tr("Flip vertically"), this); m_flipXAction = new QAction(embed::getIconPixmap("flip_x"), tr("Flip horizontally"), this); connect(editModeGroup, SIGNAL(triggered(int)), m_editor, SLOT(setEditMode(int))); - editActionsToolBar->addAction(drawAction); - editActionsToolBar->addAction(eraseAction); - editActionsToolBar->addAction(drawOutAction); + editActionsToolBar->addAction(m_drawAction); + editActionsToolBar->addAction(m_eraseAction); + editActionsToolBar->addAction(m_drawOutAction); + editActionsToolBar->addAction(m_editTanAction); editActionsToolBar->addAction(m_flipXAction); editActionsToolBar->addAction(m_flipYAction); @@ -1874,7 +2126,7 @@ AutomationEditorWindow::AutomationEditorWindow() : m_cubicHermiteAction = progression_type_group->addAction( embed::getIconPixmap("progression_cubic_hermite"), tr( "Cubic Hermite progression")); - connect(progression_type_group, SIGNAL(triggered(int)), m_editor, SLOT(setProgressionType(int))); + connect(progression_type_group, SIGNAL(triggered(int)), this, SLOT(setProgressionType(int))); // setup tension-stuff m_tensionKnob = new Knob( KnobType::Small17, this, "Tension" ); @@ -1956,8 +2208,18 @@ AutomationEditorWindow::AutomationEditorWindow() : quantizationActionsToolBar->addWidget( quantize_lbl ); quantizationActionsToolBar->addWidget( m_quantizeComboBox ); + m_resetGhostNotes = new QPushButton(m_toolBar); + m_resetGhostNotes->setIcon(embed::getIconPixmap("clear_ghost_note")); + m_resetGhostNotes->setToolTip(tr("Clear ghost notes")); + m_resetGhostNotes->setEnabled(true); + + connect(m_resetGhostNotes, &QPushButton::pressed, m_editor, &AutomationEditor::resetGhostNotes); + + quantizationActionsToolBar->addSeparator(); + quantizationActionsToolBar->addWidget(m_resetGhostNotes); + // Setup our actual window - setFocusPolicy( Qt::StrongFocus ); + setFocusPolicy(Qt::StrongFocus); setFocus(); setWindowIcon( embed::getIconPixmap( "automation" ) ); setAcceptDrops( true ); @@ -2014,6 +2276,7 @@ void AutomationEditorWindow::setCurrentClip(AutomationClip* clip) connect(m_flipYAction, SIGNAL(triggered()), clip, SLOT(flipY())); } + updateEditTanButton(); emit currentClipChanged(); } @@ -2102,5 +2365,17 @@ void AutomationEditorWindow::updateWindowTitle() setWindowTitle( tr( "Automation Editor - %1" ).arg( m_editor->m_clip->name() ) ); } +void AutomationEditorWindow::setProgressionType(int progType) +{ + m_editor->setProgressionType(progType); + updateEditTanButton(); +} + +void AutomationEditorWindow::updateEditTanButton() +{ + auto progType = currentClip()->progressionType(); + m_editTanAction->setEnabled(AutomationClip::supportsTangentEditing(progType)); + if (!m_editTanAction->isEnabled() && m_editTanAction->isChecked()) { m_drawAction->trigger(); } +} } // namespace lmms::gui diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index cef2205d266..67fb940aa52 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -119,15 +119,6 @@ const int INITIAL_START_KEY = Octave::Octave_4 + Key::C; const int NUM_EVEN_LENGTHS = 6; const int NUM_TRIPLET_LENGTHS = 5; - - -QPixmap * PianoRoll::s_toolDraw = nullptr; -QPixmap * PianoRoll::s_toolErase = nullptr; -QPixmap * PianoRoll::s_toolSelect = nullptr; -QPixmap * PianoRoll::s_toolMove = nullptr; -QPixmap * PianoRoll::s_toolOpen = nullptr; -QPixmap* PianoRoll::s_toolKnife = nullptr; - SimpleTextFloat * PianoRoll::s_textFloat = nullptr; static std::array s_noteStrings { @@ -264,32 +255,6 @@ PianoRoll::PianoRoll() : m_semiToneMarkerMenu->addAction( unmarkAllAction ); m_semiToneMarkerMenu->addAction( copyAllNotesAction ); - // init pixmaps - if( s_toolDraw == nullptr ) - { - s_toolDraw = new QPixmap( embed::getIconPixmap( "edit_draw" ) ); - } - if( s_toolErase == nullptr ) - { - s_toolErase= new QPixmap( embed::getIconPixmap( "edit_erase" ) ); - } - if( s_toolSelect == nullptr ) - { - s_toolSelect = new QPixmap( embed::getIconPixmap( "edit_select" ) ); - } - if( s_toolMove == nullptr ) - { - s_toolMove = new QPixmap( embed::getIconPixmap( "edit_move" ) ); - } - if( s_toolOpen == nullptr ) - { - s_toolOpen = new QPixmap( embed::getIconPixmap( "automation" ) ); - } - if (s_toolKnife == nullptr) - { - s_toolKnife = new QPixmap(embed::getIconPixmap("edit_knife")); - } - // init text-float if( s_textFloat == nullptr ) { @@ -1670,6 +1635,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) } detuningClip = n->detuning()->automationClip(); connect(detuningClip.data(), SIGNAL(dataChanged()), this, SLOT(update())); + getGUI()->automationEditor()->setGhostMidiClip(m_midiClip); getGUI()->automationEditor()->open(detuningClip); return; } @@ -2517,7 +2483,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) // We iterate from last note in MIDI clip to the first, // chronologically auto it = notes.rbegin(); - for( int i = 0; i < notes.size(); ++i ) + while (it != notes.rend()) { Note* n = *it; @@ -3498,11 +3464,15 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) // is the note in visible area? if (note->key() > bottomKey && note->key() <= topKey) { - // we've done and checked all, let's draw the note + // We've done and checked all, let's draw the note with + // the appropriate color + const auto fillColor = note->type() == Note::Type::Regular ? m_noteColor : m_stepNoteColor; + drawNoteRect( p, x + m_whiteKeyWidth, noteYPos(note->key()), note_width, - note, m_noteColor, m_noteTextColor, m_selectedNoteColor, - m_noteOpacity, m_noteBorders, drawNoteNames); + note, fillColor, m_noteTextColor, m_selectedNoteColor, + m_noteOpacity, m_noteBorders, drawNoteNames + ); } // draw note editing stuff @@ -3699,21 +3669,29 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) case EditMode::Draw: if( m_mouseDownRight ) { - cursor = s_toolErase; + cursor = &m_toolErase; } else if( m_action == Action::MoveNote ) { - cursor = s_toolMove; + cursor = &m_toolMove; } else { - cursor = s_toolDraw; + cursor = &m_toolDraw; } break; - case EditMode::Erase: cursor = s_toolErase; break; - case EditMode::Select: cursor = s_toolSelect; break; - case EditMode::Detuning: cursor = s_toolOpen; break; - case EditMode::Knife: cursor = s_toolKnife; break; + case EditMode::Erase: + cursor = &m_toolErase; + break; + case EditMode::Select: + cursor = &m_toolSelect; + break; + case EditMode::Detuning: + cursor = &m_toolOpen; + break; + case EditMode::Knife: + cursor = &m_toolKnife; + break; } QPoint mousePosition = mapFromGlobal( QCursor::pos() ); if( cursor != nullptr && mousePosition.y() > keyAreaTop() && mousePosition.x() > noteEditLeft()) diff --git a/src/gui/editors/TimeLineWidget.cpp b/src/gui/editors/TimeLineWidget.cpp index 423485a2589..049a8623ffb 100644 --- a/src/gui/editors/TimeLineWidget.cpp +++ b/src/gui/editors/TimeLineWidget.cpp @@ -45,9 +45,6 @@ namespace constexpr int MIN_BAR_LABEL_DISTANCE = 35; } - -QPixmap * TimeLineWidget::s_posMarkerPixmap = nullptr; - TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, Song::PlayPos & pos, const TimePos & begin, Song::PlayMode mode, QWidget * parent ) : @@ -80,16 +77,10 @@ TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, m_loopPos[0] = 0; m_loopPos[1] = DefaultTicksPerBar; - if( s_posMarkerPixmap == nullptr ) - { - s_posMarkerPixmap = new QPixmap( embed::getIconPixmap( - "playpos_marker" ) ); - } - setAttribute( Qt::WA_OpaquePaintEvent, true ); move( 0, yoff ); - m_xOffset -= s_posMarkerPixmap->width() / 2; + m_xOffset -= m_posMarkerPixmap.width() / 2; setMouseTracking(true); m_pos.m_timeLine = this; @@ -119,7 +110,7 @@ TimeLineWidget::~TimeLineWidget() void TimeLineWidget::setXOffset(const int x) { m_xOffset = x; - if (s_posMarkerPixmap != nullptr) { m_xOffset -= s_posMarkerPixmap->width() / 2; } + m_xOffset -= m_posMarkerPixmap.width() / 2; } @@ -245,7 +236,7 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) p.fillRect( 0, 0, width(), height(), p.background() ); // Clip so that we only draw everything starting from the offset - const int leftMargin = m_xOffset + s_posMarkerPixmap->width() / 2; + const int leftMargin = m_xOffset + m_posMarkerPixmap.width() / 2; p.setClipRect(leftMargin, 0, width() - leftMargin, height() ); // Draw the loop rectangle @@ -273,8 +264,8 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) QColor const & barNumberColor = getBarNumberColor(); bar_t barNumber = m_begin.getBar(); - int const x = m_xOffset + s_posMarkerPixmap->width() / 2 - - ( ( static_cast( m_begin * m_ppb ) / TimePos::ticksPerBar() ) % static_cast( m_ppb ) ); + int const x = m_xOffset + m_posMarkerPixmap.width() / 2 + - ((static_cast(m_begin * m_ppb) / TimePos::ticksPerBar()) % static_cast(m_ppb)); // Double the interval between bar numbers until they are far enough appart int barLabelInterval = 1; @@ -307,12 +298,12 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) p.drawRect( innerRectangle ); // Only draw the position marker if the position line is in view - if (m_posMarkerX >= m_xOffset && m_posMarkerX < width() - s_posMarkerPixmap->width() / 2) + if (m_posMarkerX >= m_xOffset && m_posMarkerX < width() - m_posMarkerPixmap.width() / 2) { // Let the position marker extrude to the left p.setClipping(false); p.setOpacity(0.6); - p.drawPixmap(m_posMarkerX, height() - s_posMarkerPixmap->height(), *s_posMarkerPixmap); + p.drawPixmap(m_posMarkerX, height() - m_posMarkerPixmap.height(), m_posMarkerPixmap); } } @@ -328,13 +319,13 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) if( event->button() == Qt::LeftButton && !(event->modifiers() & Qt::ShiftModifier) ) { m_action = Action::MovePositionMarker; - if( event->x() - m_xOffset < s_posMarkerPixmap->width() ) + if (event->x() - m_xOffset < m_posMarkerPixmap.width()) { m_moveXOff = event->x() - m_xOffset; } else { - m_moveXOff = s_posMarkerPixmap->width() / 2; + m_moveXOff = m_posMarkerPixmap.width() / 2; } } else if( event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier) ) @@ -344,7 +335,7 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) } else if( event->button() == Qt::RightButton ) { - m_moveXOff = s_posMarkerPixmap->width() / 2; + m_moveXOff = m_posMarkerPixmap.width() / 2; const TimePos t = m_begin + static_cast( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * TimePos::ticksPerBar() / m_ppb ); const TimePos loopMid = ( m_loopPos[0] + m_loopPos[1] ) / 2; diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index edb6c99c797..2a192832601 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -79,25 +79,11 @@ const int LFO_AMOUNT_KNOB_X = LFO_SPEED_KNOB_X+KNOB_X_SPACING; const int LFO_SHAPES_X = LFO_GRAPH_X;//PREDELAY_KNOB_X; const int LFO_SHAPES_Y = LFO_GRAPH_Y + 50; - -QPixmap * EnvelopeAndLfoView::s_envGraph = nullptr; -QPixmap * EnvelopeAndLfoView::s_lfoGraph = nullptr; - - - EnvelopeAndLfoView::EnvelopeAndLfoView( QWidget * _parent ) : QWidget( _parent ), ModelView( nullptr, this ), m_params( nullptr ) { - if( s_envGraph == nullptr ) - { - s_envGraph = new QPixmap( embed::getIconPixmap( "envelope_graph" ) ); - } - if( s_lfoGraph == nullptr ) - { - s_lfoGraph = new QPixmap( embed::getIconPixmap( "lfo_graph" ) ); - } m_predelayKnob = new Knob( KnobType::Bright26, this ); m_predelayKnob->setLabel( tr( "DEL" ) ); @@ -277,8 +263,7 @@ void EnvelopeAndLfoView::mousePressEvent( QMouseEvent * _me ) return; } - if( QRect( ENV_GRAPH_X, ENV_GRAPH_Y, s_envGraph->width(), - s_envGraph->height() ).contains( _me->pos() ) == true ) + if (QRect(ENV_GRAPH_X, ENV_GRAPH_Y, m_envGraph.width(), m_envGraph.height()).contains(_me->pos())) { if( m_params->m_amountModel.value() < 1.0f ) { @@ -289,8 +274,7 @@ void EnvelopeAndLfoView::mousePressEvent( QMouseEvent * _me ) m_params->m_amountModel.setValue( 0.0f ); } } - else if( QRect( LFO_GRAPH_X, LFO_GRAPH_Y, s_lfoGraph->width(), - s_lfoGraph->height() ).contains( _me->pos() ) == true ) + else if (QRect(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph.width(), m_lfoGraph.height()).contains(_me->pos())) { if( m_params->m_lfoAmountModel.value() < 1.0f ) { @@ -351,10 +335,9 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) p.setRenderHint( QPainter::Antialiasing ); // draw envelope-graph - p.drawPixmap( ENV_GRAPH_X, ENV_GRAPH_Y, *s_envGraph ); + p.drawPixmap(ENV_GRAPH_X, ENV_GRAPH_Y, m_envGraph); // draw LFO-graph - p.drawPixmap( LFO_GRAPH_X, LFO_GRAPH_Y, *s_lfoGraph ); - + p.drawPixmap(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph); p.setFont( pointSize<8>( p.font() ) ); @@ -368,8 +351,8 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) const QColor end_points_color( 0x99, 0xAF, 0xFF ); const QColor end_points_bg_color( 0, 0, 2 ); - const int y_base = ENV_GRAPH_Y + s_envGraph->height() - 3; - const int avail_height = s_envGraph->height() - 6; + const int y_base = ENV_GRAPH_Y + m_envGraph.height() - 3; + const int avail_height = m_envGraph.height() - 6; int x1 = static_cast( m_predelayKnob->value() * TIME_UNIT_WIDTH ); int x2 = x1 + static_cast( m_attackKnob->value() * TIME_UNIT_WIDTH ); @@ -422,9 +405,8 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) p.fillRect( x5 - 1, y_base - 2, 4, 4, end_points_bg_color ); p.fillRect( x5, y_base - 1, 2, 2, end_points_color ); - - int LFO_GRAPH_W = s_lfoGraph->width() - 3; // subtract border - int LFO_GRAPH_H = s_lfoGraph->height() - 6; // subtract border + int LFO_GRAPH_W = m_lfoGraph.width() - 3; // subtract border + int LFO_GRAPH_H = m_lfoGraph.height() - 6; // subtract border int graph_x_base = LFO_GRAPH_X + 2; int graph_y_base = LFO_GRAPH_Y + 3 + LFO_GRAPH_H / 2; @@ -505,11 +487,8 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) int ms_per_osc = static_cast( SECS_PER_LFO_OSCILLATION * m_lfoSpeedKnob->value() * 1000.0f ); - p.drawText( LFO_GRAPH_X + 4, LFO_GRAPH_Y + s_lfoGraph->height() - 6, - tr( "ms/LFO:" ) ); - p.drawText( LFO_GRAPH_X + 52, LFO_GRAPH_Y + s_lfoGraph->height() - 6, - QString::number( ms_per_osc ) ); - + p.drawText(LFO_GRAPH_X + 4, LFO_GRAPH_Y + m_lfoGraph.height() - 6, tr("ms/LFO:")); + p.drawText(LFO_GRAPH_X + 52, LFO_GRAPH_Y + m_lfoGraph.height() - 6, QString::number(ms_per_osc)); } @@ -536,4 +515,4 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() } // namespace gui -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index d20cbcac52e..c8882898bc2 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -67,15 +67,6 @@ auto WhiteKeys = std::array Key::C, Key::D, Key::E, Key::F, Key::G, Key::A, Key::H } ; - -QPixmap * PianoView::s_whiteKeyPm = nullptr; /*!< A white key released */ -QPixmap * PianoView::s_blackKeyPm = nullptr; /*!< A black key released */ -QPixmap * PianoView::s_whiteKeyPressedPm = nullptr; /*!< A white key pressed */ -QPixmap * PianoView::s_blackKeyPressedPm = nullptr; /*!< A black key pressed */ -QPixmap * PianoView::s_whiteKeyDisabledPm = nullptr; /*!< A white key disabled */ -QPixmap * PianoView::s_blackKeyDisabledPm = nullptr; /*!< A black key disabled */ - - const int PIANO_BASE = 11; /*!< The height of the root note display */ const int PW_WHITE_KEY_WIDTH = 10; /*!< The width of a white key */ const int PW_BLACK_KEY_WIDTH = 8; /*!< The width of a black key */ @@ -99,31 +90,6 @@ PianoView::PianoView(QWidget *parent) : m_lastKey(-1), /*!< The last key displayed? */ m_movedNoteModel(nullptr) /*!< Key marker which is being moved */ { - if (s_whiteKeyPm == nullptr) - { - s_whiteKeyPm = new QPixmap(embed::getIconPixmap("white_key")); - } - if (s_blackKeyPm == nullptr) - { - s_blackKeyPm = new QPixmap(embed::getIconPixmap("black_key")); - } - if (s_whiteKeyPressedPm == nullptr) - { - s_whiteKeyPressedPm = new QPixmap(embed::getIconPixmap("white_key_pressed")); - } - if (s_blackKeyPressedPm == nullptr) - { - s_blackKeyPressedPm = new QPixmap(embed::getIconPixmap("black_key_pressed")); - } - if (s_whiteKeyDisabledPm == nullptr) - { - s_whiteKeyDisabledPm = new QPixmap(embed::getIconPixmap("white_key_disabled")); - } - if (s_blackKeyDisabledPm == nullptr) - { - s_blackKeyDisabledPm = new QPixmap(embed::getIconPixmap("black_key_disabled")); - } - setAttribute(Qt::WA_OpaquePaintEvent, true); setFocusPolicy(Qt::StrongFocus); @@ -894,16 +860,16 @@ void PianoView::paintEvent( QPaintEvent * ) { if (m_piano && m_piano->isKeyPressed(cur_key)) { - p.drawPixmap(x, PIANO_BASE, *s_whiteKeyPressedPm); + p.drawPixmap(x, PIANO_BASE, m_whiteKeyPressedPm); } else { - p.drawPixmap(x, PIANO_BASE, *s_whiteKeyPm); + p.drawPixmap(x, PIANO_BASE, m_whiteKeyPm); } } else { - p.drawPixmap(x, PIANO_BASE, *s_whiteKeyDisabledPm); + p.drawPixmap(x, PIANO_BASE, m_whiteKeyDisabledPm); } x += PW_WHITE_KEY_WIDTH; @@ -928,16 +894,16 @@ void PianoView::paintEvent( QPaintEvent * ) { if (m_piano && m_piano->isKeyPressed(startKey)) { - p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPressedPm); + p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPressedPm); } else { - p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPm); + p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPm); } } else { - p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyDisabledPm); + p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyDisabledPm); } } @@ -951,16 +917,16 @@ void PianoView::paintEvent( QPaintEvent * ) { if (m_piano && m_piano->isKeyPressed(cur_key)) { - p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPressedPm); + p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPressedPm); } else { - p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPm); + p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPm); } } else { - p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyDisabledPm); + p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyDisabledPm); } x += PW_WHITE_KEY_WIDTH; white_cnt = 0; diff --git a/src/gui/modals/EffectSelectDialog.cpp b/src/gui/modals/EffectSelectDialog.cpp index 31ffd772856..993052fab6a 100644 --- a/src/gui/modals/EffectSelectDialog.cpp +++ b/src/gui/modals/EffectSelectDialog.cpp @@ -98,12 +98,13 @@ EffectSelectDialog::EffectSelectDialog( QWidget * _parent ) : m_model.setSourceModel( &m_sourceModel ); m_model.setFilterCaseSensitivity( Qt::CaseInsensitive ); - connect( ui->filterEdit, SIGNAL( textChanged( const QString& ) ), - &m_model, SLOT( setFilterFixedString( const QString& ) ) ); - connect( ui->filterEdit, SIGNAL( textChanged( const QString& ) ), - this, SLOT(updateSelection())); - connect( ui->filterEdit, SIGNAL( textChanged( const QString& ) ), - SLOT(sortAgain())); + ui->filterEdit->setPlaceholderText(tr("Search")); + ui->filterEdit->setClearButtonEnabled(true); + ui->filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); + + connect(ui->filterEdit, &QLineEdit::textChanged, &m_model, &QSortFilterProxyModel::setFilterFixedString); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::updateSelection); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::sortAgain); ui->pluginList->setModel( &m_model ); diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 0266285a7a4..209422563bc 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -23,14 +23,15 @@ */ +#include #include +#include #include #include #include #include #include -#include "AudioDeviceSetupWidget.h" #include "AudioEngine.h" #include "debug.h" #include "embed.h" @@ -79,13 +80,12 @@ inline void labelWidget(QWidget * w, const QString & txt) auto title = new QLabel(txt, w); QFont f = title->font(); f.setBold(true); - title->setFont(pointSize<12>(f)); + title->setFont(f); + QBoxLayout * boxLayout = dynamic_cast(w->layout()); + assert(boxLayout); - assert(dynamic_cast(w->layout()) != nullptr); - - dynamic_cast(w->layout())->addSpacing(5); - dynamic_cast(w->layout())->addWidget(title); + boxLayout->addWidget(title); } @@ -162,15 +162,10 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // TODO: Equivalent to the new setWindowFlag(Qt::WindowContextHelpButtonHint, false) setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); setModal(true); - setFixedSize(454, 400); Engine::projectJournal()->setJournalling(false); - // Constants for positioning LED check boxes. - const int XDelta = 10; - const int YDelta = 18; - // Main widget. auto main_w = new QWidget(this); @@ -191,7 +186,8 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Settings widget. auto settings_w = new QWidget(main_w); - settings_w->setFixedSize(360, 360); + + QVBoxLayout * settingsLayout = new QVBoxLayout(settings_w); // General widget. auto general_w = new QWidget(settings_w); @@ -211,77 +207,79 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Path selectors layout. auto generalControlsLayout = new QVBoxLayout; generalControlsLayout->setSpacing(10); + generalControlsLayout->setContentsMargins(0, 0, 0, 0); - auto addLedCheckBox = [&XDelta, &YDelta, this](const QString& ledText, TabWidget* tw, int& counter, - bool initialState, const char* toggledSlot, bool showRestartWarning) { - auto checkBox = new LedCheckBox(ledText, tw); - counter++; - checkBox->move(XDelta, YDelta * counter); + auto addCheckBox = [&](const QString& ledText, QWidget* parent, QBoxLayout * layout, + bool initialState, const char* toggledSlot, bool showRestartWarning) -> QCheckBox * { + auto checkBox = new QCheckBox(ledText, parent); checkBox->setChecked(initialState); connect(checkBox, SIGNAL(toggled(bool)), this, toggledSlot); + if (showRestartWarning) { connect(checkBox, SIGNAL(toggled(bool)), this, SLOT(showRestartWarning())); } - }; - int counter = 0; + if (layout) + { + layout->addWidget(checkBox); + } + + return checkBox; + }; // GUI tab. - auto gui_tw = new TabWidget(tr("Graphical user interface (GUI)"), generalControls); + QGroupBox * guiGroupBox = new QGroupBox(tr("Graphical user interface (GUI)"), generalControls); + QVBoxLayout * guiGroupLayout = new QVBoxLayout(guiGroupBox); - addLedCheckBox(tr("Display volume as dBFS "), gui_tw, counter, + addCheckBox(tr("Display volume as dBFS "), guiGroupBox, guiGroupLayout, m_displaydBFS, SLOT(toggleDisplaydBFS(bool)), true); - addLedCheckBox(tr("Enable tooltips"), gui_tw, counter, + addCheckBox(tr("Enable tooltips"), guiGroupBox, guiGroupLayout, m_tooltips, SLOT(toggleTooltips(bool)), true); - addLedCheckBox(tr("Enable master oscilloscope by default"), gui_tw, counter, + addCheckBox(tr("Enable master oscilloscope by default"), guiGroupBox, guiGroupLayout, m_displayWaveform, SLOT(toggleDisplayWaveform(bool)), true); - addLedCheckBox(tr("Enable all note labels in piano roll"), gui_tw, counter, + addCheckBox(tr("Enable all note labels in piano roll"), guiGroupBox, guiGroupLayout, m_printNoteLabels, SLOT(toggleNoteLabels(bool)), false); - addLedCheckBox(tr("Enable compact track buttons"), gui_tw, counter, + addCheckBox(tr("Enable compact track buttons"), guiGroupBox, guiGroupLayout, m_compactTrackButtons, SLOT(toggleCompactTrackButtons(bool)), true); - addLedCheckBox(tr("Enable one instrument-track-window mode"), gui_tw, counter, + addCheckBox(tr("Enable one instrument-track-window mode"), guiGroupBox, guiGroupLayout, m_oneInstrumentTrackWindow, SLOT(toggleOneInstrumentTrackWindow(bool)), true); - addLedCheckBox(tr("Show sidebar on the right-hand side"), gui_tw, counter, + addCheckBox(tr("Show sidebar on the right-hand side"), guiGroupBox, guiGroupLayout, m_sideBarOnRight, SLOT(toggleSideBarOnRight(bool)), true); - addLedCheckBox(tr("Let sample previews continue when mouse is released"), gui_tw, counter, + addCheckBox(tr("Let sample previews continue when mouse is released"), guiGroupBox, guiGroupLayout, m_letPreviewsFinish, SLOT(toggleLetPreviewsFinish(bool)), false); - addLedCheckBox(tr("Mute automation tracks during solo"), gui_tw, counter, + addCheckBox(tr("Mute automation tracks during solo"), guiGroupBox, guiGroupLayout, m_soloLegacyBehavior, SLOT(toggleSoloLegacyBehavior(bool)), false); - addLedCheckBox(tr("Show warning when deleting tracks"), gui_tw, counter, + addCheckBox(tr("Show warning when deleting tracks"), guiGroupBox, guiGroupLayout, m_trackDeletionWarning, SLOT(toggleTrackDeletionWarning(bool)), false); - addLedCheckBox(tr("Show warning when deleting a mixer channel that is in use"), gui_tw, counter, + addCheckBox(tr("Show warning when deleting a mixer channel that is in use"), guiGroupBox, guiGroupLayout, m_mixerChannelDeletionWarning, SLOT(toggleMixerChannelDeletionWarning(bool)), false); - gui_tw->setFixedHeight(YDelta + YDelta * counter); + generalControlsLayout->addWidget(guiGroupBox); - generalControlsLayout->addWidget(gui_tw); generalControlsLayout->addSpacing(10); - - counter = 0; - // Projects tab. - auto projects_tw = new TabWidget(tr("Projects"), generalControls); + QGroupBox * projectsGroupBox = new QGroupBox(tr("Projects"), generalControls); + QVBoxLayout * projectsGroupLayout = new QVBoxLayout(projectsGroupBox); - addLedCheckBox(tr("Compress project files by default"), projects_tw, counter, + addCheckBox(tr("Compress project files by default"), projectsGroupBox, projectsGroupLayout, m_MMPZ, SLOT(toggleMMPZ(bool)), true); - addLedCheckBox(tr("Create a backup file when saving a project"), projects_tw, counter, + addCheckBox(tr("Create a backup file when saving a project"), projectsGroupBox, projectsGroupLayout, m_disableBackup, SLOT(toggleDisableBackup(bool)), false); - addLedCheckBox(tr("Reopen last project on startup"), projects_tw, counter, + addCheckBox(tr("Reopen last project on startup"), projectsGroupBox, projectsGroupLayout, m_openLastProject, SLOT(toggleOpenLastProject(bool)), false); - projects_tw->setFixedHeight(YDelta + YDelta * counter); + generalControlsLayout->addWidget(projectsGroupBox); - generalControlsLayout->addWidget(projects_tw); generalControlsLayout->addSpacing(10); - // Language tab. - auto lang_tw = new TabWidget(tr("Language"), generalControls); - lang_tw->setFixedHeight(48); - auto changeLang = new QComboBox(lang_tw); - changeLang->move(XDelta, 20); + QGroupBox * languageGroupBox = new QGroupBox(tr("Language"), generalControls); + QVBoxLayout * languageGroupLayout = new QVBoxLayout(languageGroupBox); + + auto changeLang = new QComboBox(languageGroupBox); + languageGroupLayout->addWidget(changeLang); QDir dir(ConfigManager::inst()->localeDir()); QStringList fileNames = dir.entryList(QStringList("*.qm")); @@ -333,7 +331,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : connect(changeLang, SIGNAL(currentIndexChanged(int)), this, SLOT(showRestartWarning())); - generalControlsLayout->addWidget(lang_tw); + generalControlsLayout->addWidget(languageGroupBox); generalControlsLayout->addSpacing(10); // General layout ordering. @@ -341,9 +339,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : generalControls->setLayout(generalControlsLayout); generalScroll->setWidget(generalControls); generalScroll->setWidgetResizable(true); - general_layout->addWidget(generalScroll); - general_layout->addStretch(); - + general_layout->addWidget(generalScroll, 1); @@ -357,71 +353,63 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Autosave tab. - auto auto_save_tw = new TabWidget(tr("Autosave"), performance_w); - auto_save_tw->setFixedHeight(106); + QGroupBox * autoSaveBox = new QGroupBox(tr("Autosave"), performance_w); + QVBoxLayout * autoSaveLayout = new QVBoxLayout(autoSaveBox); + QHBoxLayout * autoSaveSubLayout = new QHBoxLayout(); - m_saveIntervalSlider = new QSlider(Qt::Horizontal, auto_save_tw); + m_saveIntervalSlider = new QSlider(Qt::Horizontal, autoSaveBox); m_saveIntervalSlider->setValue(m_saveInterval); m_saveIntervalSlider->setRange(1, 20); m_saveIntervalSlider->setTickInterval(1); m_saveIntervalSlider->setPageStep(1); - m_saveIntervalSlider->setGeometry(10, 18, 340, 18); m_saveIntervalSlider->setTickPosition(QSlider::TicksBelow); connect(m_saveIntervalSlider, SIGNAL(valueChanged(int)), this, SLOT(setAutoSaveInterval(int))); - m_saveIntervalLbl = new QLabel(auto_save_tw); - m_saveIntervalLbl->setGeometry(10, 40, 200, 24); + auto autoSaveResetBtn = new QPushButton(embed::getIconPixmap("reload"), "", autoSaveBox); + autoSaveResetBtn->setFixedSize(32, 32); + connect(autoSaveResetBtn, SIGNAL(clicked()), + this, SLOT(resetAutoSave())); + + autoSaveSubLayout->addWidget(m_saveIntervalSlider); + autoSaveSubLayout->addWidget(autoSaveResetBtn); + + autoSaveLayout->addLayout(autoSaveSubLayout); + + m_saveIntervalLbl = new QLabel(autoSaveBox); setAutoSaveInterval(m_saveIntervalSlider->value()); + autoSaveLayout->addWidget(m_saveIntervalLbl); - m_autoSave = new LedCheckBox( - tr("Enable autosave"), auto_save_tw); - m_autoSave->move(10, 70); - m_autoSave->setChecked(m_enableAutoSave); - connect(m_autoSave, SIGNAL(toggled(bool)), - this, SLOT(toggleAutoSave(bool))); - - m_runningAutoSave = new LedCheckBox( - tr("Allow autosave while playing"), auto_save_tw); - m_runningAutoSave->move(20, 88); - m_runningAutoSave->setChecked(m_enableRunningAutoSave); - connect(m_runningAutoSave, SIGNAL(toggled(bool)), - this, SLOT(toggleRunningAutoSave(bool))); - - auto autoSaveResetBtn = new QPushButton(embed::getIconPixmap("reload"), "", auto_save_tw); - autoSaveResetBtn->setGeometry(320, 70, 28, 28); - connect(autoSaveResetBtn, SIGNAL(clicked()), - this, SLOT(resetAutoSave())); + m_autoSave = addCheckBox(tr("Enable autosave"), autoSaveBox, autoSaveLayout, + m_enableAutoSave, SLOT(toggleAutoSave(bool)), false); + + m_runningAutoSave = addCheckBox(tr("Allow autosave while playing"), autoSaveBox, autoSaveLayout, + m_enableRunningAutoSave, SLOT(toggleRunningAutoSave(bool)), false); m_saveIntervalSlider->setEnabled(m_enableAutoSave); m_runningAutoSave->setVisible(m_enableAutoSave); - counter = 0; - // UI effect vs. performance tab. - auto ui_fx_tw = new TabWidget(tr("User interface (UI) effects vs. performance"), performance_w); + QGroupBox * uiFxBox = new QGroupBox(tr("User interface (UI) effects vs. performance"), performance_w); + QVBoxLayout * uiFxLayout = new QVBoxLayout(uiFxBox); - addLedCheckBox(tr("Smooth scroll in song editor"), ui_fx_tw, counter, + addCheckBox(tr("Smooth scroll in song editor"), uiFxBox, uiFxLayout, m_smoothScroll, SLOT(toggleSmoothScroll(bool)), false); - addLedCheckBox(tr("Display playback cursor in AudioFileProcessor"), ui_fx_tw, counter, + addCheckBox(tr("Display playback cursor in AudioFileProcessor"), uiFxBox, uiFxLayout, m_animateAFP, SLOT(toggleAnimateAFP(bool)), false); - ui_fx_tw->setFixedHeight(YDelta + YDelta * counter); - - counter = 0; + // Plugins group + QGroupBox * pluginsBox = new QGroupBox(tr("Plugins"), performance_w); + QVBoxLayout * pluginsLayout = new QVBoxLayout(pluginsBox); - // Plugins tab. - auto plugins_tw = new TabWidget(tr("Plugins"), performance_w); - - m_vstEmbedLbl = new QLabel(plugins_tw); - m_vstEmbedLbl->move(XDelta, YDelta * ++counter); + m_vstEmbedLbl = new QLabel(pluginsBox); m_vstEmbedLbl->setText(tr("VST plugins embedding:")); + pluginsLayout->addWidget(m_vstEmbedLbl); - m_vstEmbedComboBox = new QComboBox(plugins_tw); - m_vstEmbedComboBox->move(XDelta, YDelta * ++counter); + m_vstEmbedComboBox = new QComboBox(pluginsBox); QStringList embedMethods = ConfigManager::availableVstEmbedMethods(); m_vstEmbedComboBox->addItem(tr("No embedding"), "none"); @@ -440,27 +428,19 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : m_vstEmbedComboBox->setCurrentIndex(m_vstEmbedComboBox->findData(m_vstEmbedMethod)); connect(m_vstEmbedComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(vstEmbedMethodChanged())); + pluginsLayout->addWidget(m_vstEmbedComboBox); - counter += 2; - - m_vstAlwaysOnTopCheckBox = new LedCheckBox( - tr("Keep plugin windows on top when not embedded"), plugins_tw); - m_vstAlwaysOnTopCheckBox->move(20, 66); - m_vstAlwaysOnTopCheckBox->setChecked(m_vstAlwaysOnTop); - m_vstAlwaysOnTopCheckBox->setVisible(m_vstEmbedMethod == "none"); - connect(m_vstAlwaysOnTopCheckBox, SIGNAL(toggled(bool)), - this, SLOT(toggleVSTAlwaysOnTop(bool))); + m_vstAlwaysOnTopCheckBox = addCheckBox(tr("Keep plugin windows on top when not embedded"), pluginsBox, pluginsLayout, + m_vstAlwaysOnTop, SLOT(toggleVSTAlwaysOnTop(bool)), false); - addLedCheckBox(tr("Keep effects running even without input"), plugins_tw, counter, + addCheckBox(tr("Keep effects running even without input"), pluginsBox, pluginsLayout, m_disableAutoQuit, SLOT(toggleDisableAutoQuit(bool)), false); - plugins_tw->setFixedHeight(YDelta + YDelta * counter); - // Performance layout ordering. - performance_layout->addWidget(auto_save_tw); - performance_layout->addWidget(ui_fx_tw); - performance_layout->addWidget(plugins_tw); + performance_layout->addWidget(autoSaveBox); + performance_layout->addWidget(uiFxBox); + performance_layout->addWidget(pluginsBox); performance_layout->addStretch(); @@ -473,17 +453,15 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : labelWidget(audio_w, tr("Audio")); - // Audio interface tab. - auto audioiface_tw = new TabWidget(tr("Audio interface"), audio_w); - audioiface_tw->setFixedHeight(56); - - m_audioInterfaces = new QComboBox(audioiface_tw); - m_audioInterfaces->setGeometry(10, 20, 240, 28); + // Audio interface group + QGroupBox * audioInterfaceBox = new QGroupBox(tr("Audio interface"), audio_w); + QVBoxLayout * audioInterfaceLayout = new QVBoxLayout(audioInterfaceBox); + m_audioInterfaces = new QComboBox(audioInterfaceBox); + audioInterfaceLayout->addWidget(m_audioInterfaces); // Ifaces-settings-widget. auto as_w = new QWidget(audio_w); - as_w->setFixedHeight(60); auto as_w_layout = new QHBoxLayout(as_w); as_w_layout->setSpacing(0); @@ -563,61 +541,58 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : this, SLOT(audioInterfaceChanged(const QString&))); // Advanced setting, hidden for now - if(false) - { - auto useNaNHandler = new LedCheckBox(tr("Use built-in NaN handler"), audio_w); - useNaNHandler->setChecked(m_NaNHandler); - } - - // HQ mode LED. - auto hqaudio = new LedCheckBox(tr("HQ mode for output audio device"), audio_w); - hqaudio->move(10, 0); - hqaudio->setChecked(m_hqAudioDev); - connect(hqaudio, SIGNAL(toggled(bool)), - this, SLOT(toggleHQAudioDev(bool))); + // // TODO Handle or remove. + // auto useNaNHandler = new LedCheckBox(tr("Use built-in NaN handler"), audio_w); + // audio_layout->addWidget(useNaNHandler); + // useNaNHandler->setChecked(m_NaNHandler); + // HQ mode checkbox + auto hqaudio = addCheckBox(tr("HQ mode for output audio device"), audioInterfaceBox, nullptr, + m_hqAudioDev, SLOT(toggleHQAudioDev(bool)), false); - // Buffer size tab. - auto bufferSize_tw = new TabWidget(tr("Buffer size"), audio_w); - auto bufferSize_layout = new QVBoxLayout(bufferSize_tw); - bufferSize_layout->setSpacing(10); - bufferSize_layout->setContentsMargins(10, 18, 10, 10); + // Buffer size group + QGroupBox * bufferSizeBox = new QGroupBox(tr("Buffer size"), audio_w); + QVBoxLayout * bufferSizeLayout = new QVBoxLayout(bufferSizeBox); + QHBoxLayout * bufferSizeSubLayout = new QHBoxLayout(); - m_bufferSizeSlider = new QSlider(Qt::Horizontal, bufferSize_tw); + m_bufferSizeSlider = new QSlider(Qt::Horizontal, bufferSizeBox); m_bufferSizeSlider->setRange(1, 128); m_bufferSizeSlider->setTickInterval(8); m_bufferSizeSlider->setPageStep(8); m_bufferSizeSlider->setValue(m_bufferSize / BUFFERSIZE_RESOLUTION); 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())); - setBufferSize(m_bufferSizeSlider->value()); + bufferSizeSubLayout->addWidget(m_bufferSizeSlider, 1); - auto bufferSize_reset_btn = new QPushButton(embed::getIconPixmap("reload"), "", bufferSize_tw); + auto bufferSize_reset_btn = new QPushButton(embed::getIconPixmap("reload"), "", bufferSizeBox); + bufferSize_reset_btn->setFixedSize(32, 32); connect(bufferSize_reset_btn, SIGNAL(clicked()), - this, SLOT(resetBufferSize())); + this, SLOT(resetBufferSize())); bufferSize_reset_btn->setToolTip( - tr("Reset to default value")); + tr("Reset to default value")); + + bufferSizeSubLayout->addWidget(bufferSize_reset_btn); + bufferSizeLayout->addLayout(bufferSizeSubLayout); + + m_bufferSizeLbl = new QLabel(bufferSizeBox); + bufferSizeLayout->addWidget(m_bufferSizeLbl); - bufferSize_layout->addWidget(m_bufferSizeSlider); - bufferSize_layout->addWidget(m_bufferSizeLbl); - bufferSize_layout->addWidget(m_bufferSizeWarnLbl); - bufferSize_layout->addWidget(bufferSize_reset_btn); + m_bufferSizeWarnLbl = new QLabel(bufferSizeBox); + m_bufferSizeWarnLbl->setWordWrap(true); + bufferSizeLayout->addWidget(m_bufferSizeWarnLbl); + + setBufferSize(m_bufferSizeSlider->value()); // Audio layout ordering. - audio_layout->addWidget(audioiface_tw); + audio_layout->addWidget(audioInterfaceBox); audio_layout->addWidget(as_w); audio_layout->addWidget(hqaudio); - audio_layout->addWidget(bufferSize_tw); + audio_layout->addWidget(bufferSizeBox); audio_layout->addStretch(); @@ -627,19 +602,17 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : auto midi_layout = new QVBoxLayout(midi_w); midi_layout->setSpacing(10); midi_layout->setContentsMargins(0, 0, 0, 0); - labelWidget(midi_w, - tr("MIDI")); + labelWidget(midi_w, tr("MIDI")); - // MIDI interface tab. - auto midiiface_tw = new TabWidget(tr("MIDI interface"), midi_w); - midiiface_tw->setFixedHeight(56); + // MIDI interface group + QGroupBox * midiInterfaceBox = new QGroupBox(tr("MIDI interface"), midi_w); + QVBoxLayout * midiInterfaceLayout = new QVBoxLayout(midiInterfaceBox); - m_midiInterfaces = new QComboBox(midiiface_tw); - m_midiInterfaces->setGeometry(10, 20, 240, 28); + m_midiInterfaces = new QComboBox(midiInterfaceBox); + midiInterfaceLayout->addWidget(m_midiInterfaces); // Ifaces-settings-widget. auto ms_w = new QWidget(midi_w); - ms_w->setFixedHeight(60); auto ms_w_layout = new QHBoxLayout(ms_w); ms_w_layout->setSpacing(0); @@ -709,12 +682,12 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : this, SLOT(midiInterfaceChanged(const QString&))); - // MIDI autoassign tab. - auto midiAutoAssign_tw = new TabWidget(tr("Automatically assign MIDI controller to selected track"), midi_w); - midiAutoAssign_tw->setFixedHeight(56); + // MIDI autoassign group + QGroupBox * midiAutoAssignBox = new QGroupBox(tr("Automatically assign MIDI controller to selected track"), midi_w); + QVBoxLayout * midiAutoAssignLayout = new QVBoxLayout(midiAutoAssignBox); - m_assignableMidiDevices = new QComboBox(midiAutoAssign_tw); - m_assignableMidiDevices->setGeometry(10, 20, 240, 28); + m_assignableMidiDevices = new QComboBox(midiAutoAssignBox); + midiAutoAssignLayout->addWidget(m_assignableMidiDevices); m_assignableMidiDevices->addItem("none"); if ( !Engine::audioEngine()->midiClient()->isRaw() ) { @@ -731,9 +704,9 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : } // MIDI layout ordering. - midi_layout->addWidget(midiiface_tw); + midi_layout->addWidget(midiInterfaceBox); midi_layout->addWidget(ms_w); - midi_layout->addWidget(midiAutoAssign_tw); + midi_layout->addWidget(midiAutoAssignBox); midi_layout->addStretch(); @@ -756,29 +729,29 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Path selectors widget. auto pathSelectors = new QWidget(paths_w); - const int txtLength = 284; - const int btnStart = 300; - // Path selectors layout. auto pathSelectorsLayout = new QVBoxLayout; pathSelectorsLayout->setSpacing(10); + pathSelectorsLayout->setContentsMargins(0, 0, 0, 0); auto addPathEntry = [&](const QString& caption, const QString& content, const char* setSlot, const char* openSlot, QLineEdit*& lineEdit, const char* pixmap = "project_open") { - auto newTw = new TabWidget(caption, pathSelectors); - newTw->setFixedHeight(48); + auto pathEntryGroupBox = new QGroupBox(caption, pathSelectors); + QHBoxLayout * pathEntryLayout = new QHBoxLayout(pathEntryGroupBox); - lineEdit = new QLineEdit(content, newTw); - lineEdit->setGeometry(10, 20, txtLength, 16); + lineEdit = new QLineEdit(content, pathEntryGroupBox); connect(lineEdit, SIGNAL(textChanged(const QString&)), this, setSlot); - auto selectBtn = new QPushButton(embed::getIconPixmap(pixmap, 16, 16), "", newTw); + pathEntryLayout->addWidget(lineEdit, 1); + + auto selectBtn = new QPushButton(embed::getIconPixmap(pixmap, 16, 16), "", pathEntryGroupBox); selectBtn->setFixedSize(24, 24); - selectBtn->move(btnStart, 16); connect(selectBtn, SIGNAL(clicked()), this, openSlot); - pathSelectorsLayout->addWidget(newTw); + pathEntryLayout->addWidget(selectBtn, 0); + + pathSelectorsLayout->addWidget(pathEntryGroupBox); pathSelectorsLayout->addSpacing(10); }; @@ -824,24 +797,32 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : pathsScroll->setWidget(pathSelectors); pathsScroll->setWidgetResizable(true); - paths_layout->addWidget(pathsScroll); + paths_layout->addWidget(pathsScroll, 1); paths_layout->addStretch(); + // Add all main widgets to the layout of the settings widget + // This is needed so that we automatically get the correct sizes. + settingsLayout->addWidget(general_w); + settingsLayout->addWidget(performance_w); + settingsLayout->addWidget(audio_w); + settingsLayout->addWidget(midi_w); + settingsLayout->addWidget(paths_w); + // Major tabs ordering. m_tabBar->addTab(general_w, - tr("General"), 0, false, true)->setIcon( + tr("General"), 0, false, true, false)->setIcon( embed::getIconPixmap("setup_general")); m_tabBar->addTab(performance_w, - tr("Performance"), 1, false, true)->setIcon( + tr("Performance"), 1, false, true, false)->setIcon( embed::getIconPixmap("setup_performance")); m_tabBar->addTab(audio_w, - tr("Audio"), 2, false, true)->setIcon( + tr("Audio"), 2, false, true, false)->setIcon( embed::getIconPixmap("setup_audio")); m_tabBar->addTab(midi_w, - tr("MIDI"), 3, false, true)->setIcon( + tr("MIDI"), 3, false, true, false)->setIcon( embed::getIconPixmap("setup_midi")); m_tabBar->addTab(paths_w, - tr("Paths"), 4, true, true)->setIcon( + tr("Paths"), 4, true, true, false)->setIcon( embed::getIconPixmap("setup_directories")); m_tabBar->setActiveTab(static_cast(tab_to_open)); @@ -884,11 +865,14 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : extras_layout->addSpacing(10); // Vertical layout ordering. - vlayout->addWidget(main_w); + vlayout->addWidget(main_w, 1); vlayout->addSpacing(10); vlayout->addWidget(extras_w); vlayout->addSpacing(10); + // Ensure that we cannot make the dialog smaller than it wants to be + setMinimumWidth(width()); + show(); } @@ -1182,10 +1166,14 @@ 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) + // 'value' is not a power of 2 (for value > 0) and under 256. On buffer sizes larger than 256 + // lmms works with chunks of size 256 and only the final mix will use the actual buffer size. + // Plugins don't see a larger buffer size than 256 so anything larger than this is functionally + // a 'power of 2' value. + if(((value & (value - 1)) != 0) && value < 256) { text += "
  • " + tr("The currently selected value is not a power of 2 " - "(32, 64, 128, 256, 512, 1024, ...). Some plugins may not be available.") + "
  • "; + "(32, 64, 128, 256). Some plugins may not be available.") + ""; } if(value <= 32) { diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 87c0f044944..8087af42335 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -227,7 +227,7 @@ void InstrumentTrackView::createMixerLine() auto channel = Engine::mixer()->mixerChannel(channelIndex); channel->m_name = getTrack()->name(); - if (getTrack()->useColor()) { channel->setColor (getTrack()->color()); } + channel->setColor(getTrack()->color()); assignMixerLine(channelIndex); } diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 8516eb5c2a9..ddb68ee998e 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -228,7 +228,7 @@ void SampleTrackView::createMixerLine() auto channel = Engine::mixer()->mixerChannel(channelIndex); channel->m_name = getTrack()->name(); - if (getTrack()->useColor()) { channel->setColor (getTrack()->color()); } + channel->setColor(getTrack()->color()); assignMixerLine(channelIndex); } diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index fa1a651f613..e846370e69c 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -172,11 +172,11 @@ void TrackOperationsWidget::paintEvent( QPaintEvent * pe ) p.fillRect(rect(), palette().brush(QPalette::Window)); - if( m_trackView->getTrack()->useColor() && ! m_trackView->getTrack()->getMutedModel()->value() ) + if (m_trackView->getTrack()->color().has_value() && !m_trackView->getTrack()->getMutedModel()->value()) { QRect coloredRect( 0, 0, 10, m_trackView->getTrack()->getHeight() ); - - p.fillRect( coloredRect, m_trackView->getTrack()->color() ); + + p.fillRect(coloredRect, m_trackView->getTrack()->color().value()); } p.drawPixmap(2, 2, embed::getIconPixmap(m_trackView->isMovingTrack() ? "track_op_grip_c" : "track_op_grip")); @@ -195,7 +195,7 @@ bool TrackOperationsWidget::confirmRemoval() QString messageTitleRemoveTrack = tr("Confirm removal"); QString askAgainText = tr("Don't ask again"); auto askAgainCheckBox = new QCheckBox(askAgainText, nullptr); - connect(askAgainCheckBox, &QCheckBox::stateChanged, [this](int state){ + connect(askAgainCheckBox, &QCheckBox::stateChanged, [](int state){ // Invert button state, if it's checked we *shouldn't* ask again ConfigManager::inst()->setValue("ui", "trackdeletionwarning", state ? "0" : "1"); }); @@ -265,15 +265,15 @@ void TrackOperationsWidget::removeTrack() void TrackOperationsWidget::selectTrackColor() { - QColor new_color = ColorChooser( this ).withPalette( ColorChooser::Palette::Track )-> \ - getColor( m_trackView->getTrack()->color() ); + const auto newColor = ColorChooser{this} + .withPalette(ColorChooser::Palette::Track) + ->getColor(m_trackView->getTrack()->color().value_or(Qt::white)); - if( ! new_color.isValid() ) - { return; } + if (!newColor.isValid()) { return; } - auto track = m_trackView->getTrack(); + const auto track = m_trackView->getTrack(); track->addJournalCheckPoint(); - track->setColor(new_color); + track->setColor(newColor); Engine::getSong()->setModified(); } @@ -281,7 +281,7 @@ void TrackOperationsWidget::resetTrackColor() { auto track = m_trackView->getTrack(); track->addJournalCheckPoint(); - track->resetColor(); + track->setColor(std::nullopt); Engine::getSong()->setModified(); } @@ -298,16 +298,13 @@ void TrackOperationsWidget::resetClipColors() { auto track = m_trackView->getTrack(); track->addJournalCheckPoint(); - for (auto clip: track->getClips()) + for (auto clip : track->getClips()) { - clip->useCustomClipColor(false); + clip->setColor(std::nullopt); } Engine::getSong()->setModified(); } - - - /*! \brief Update the trackOperationsWidget context menu * * For all track types, we have the Clone and Remove options. diff --git a/src/gui/widgets/BarModelEditor.cpp b/src/gui/widgets/BarModelEditor.cpp new file mode 100644 index 00000000000..4b02c963461 --- /dev/null +++ b/src/gui/widgets/BarModelEditor.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include + + +namespace lmms::gui +{ + +BarModelEditor::BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + FloatModelEditorBase(DirectionOfManipulation::Horizontal, parent), + m_text(text), + m_backgroundBrush(palette().base()), + m_barBrush(palette().button()), + m_textColor(palette().text().color()) +{ + setModel(floatModel); +} + +QSizePolicy BarModelEditor::sizePolicy() const +{ + return QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize BarModelEditor::minimumSizeHint() const +{ + auto const fm = fontMetrics(); + return QSize(50, fm.height() + 6); +} + +QSize BarModelEditor::sizeHint() const +{ + return minimumSizeHint(); +} + +QBrush const & BarModelEditor::getBackgroundBrush() const +{ + return m_backgroundBrush; +} + +void BarModelEditor::setBackgroundBrush(QBrush const & backgroundBrush) +{ + m_backgroundBrush = backgroundBrush; +} + +QBrush const & BarModelEditor::getBarBrush() const +{ + return m_barBrush; +} + +void BarModelEditor::setBarBrush(QBrush const & barBrush) +{ + m_barBrush = barBrush; +} + +QColor const & BarModelEditor::getTextColor() const +{ + return m_textColor; +} + +void BarModelEditor::setTextColor(QColor const & textColor) +{ + m_textColor = textColor; +} + +void BarModelEditor::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + QRect const r = rect(); + + QPainter painter(this); + + // Paint the base rectangle into which the bar and the text go + QBrush const & backgroundBrush = getBackgroundBrush(); + painter.setPen(backgroundBrush.color()); + painter.setBrush(backgroundBrush); + painter.drawRect(r); + + + // Paint the bar + // Compute the percentage as: + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + int const margin = 3; + QMargins const margins(margin, margin, margin, margin); + QRect const valueRect = r.marginsRemoved(margins); + + QBrush const & barBrush = getBarBrush(); + painter.setPen(barBrush.color()); + painter.setBrush(barBrush); + QPoint const startPoint = valueRect.topLeft(); + QPoint endPoint = valueRect.bottomRight(); + endPoint.setX(startPoint.x() + percentage * (endPoint.x() - startPoint.x())); + + painter.drawRect(QRect(startPoint, endPoint)); + + + // Draw the text into the value rectangle but move it slightly to the right + QRect const textRect = valueRect.marginsRemoved(QMargins(3, 0, 0, 0)); + + // Elide the text if needed + auto const fm = fontMetrics(); + QString const elidedText = fm.elidedText(m_text, Qt::ElideRight, textRect.width()); + + // Now draw the text + painter.setPen(getTextColor()); + painter.drawText(textRect, elidedText); +} + +} // namespace lmms::gui diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index 2377a37abf8..ccc0c675b0d 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -38,11 +38,6 @@ namespace lmms::gui { - -QPixmap * ComboBox::s_background = nullptr; -QPixmap * ComboBox::s_arrow = nullptr; -QPixmap * ComboBox::s_arrowSelected = nullptr; - const int CB_ARROW_BTN_WIDTH = 18; @@ -54,21 +49,6 @@ ComboBox::ComboBox( QWidget * _parent, const QString & _name ) : { setFixedHeight( ComboBox::DEFAULT_HEIGHT ); - if( s_background == nullptr ) - { - s_background = new QPixmap( embed::getIconPixmap( "combobox_bg" ) ); - } - - if( s_arrow == nullptr ) - { - s_arrow = new QPixmap( embed::getIconPixmap( "combobox_arrow" ) ); - } - - if( s_arrowSelected == nullptr ) - { - s_arrowSelected = new QPixmap( embed::getIconPixmap( "combobox_arrow_selected" ) ); - } - setFont( pointSize<9>( font() ) ); connect( &m_menu, SIGNAL(triggered(QAction*)), @@ -172,7 +152,7 @@ void ComboBox::paintEvent( QPaintEvent * _pe ) { QPainter p( this ); - p.fillRect( 2, 2, width()-2, height()-4, *s_background ); + p.fillRect(2, 2, width() - 2, height() - 4, m_background); QColor shadow = palette().shadow().color(); QColor highlight = palette().highlight().color(); @@ -194,9 +174,9 @@ void ComboBox::paintEvent( QPaintEvent * _pe ) style()->drawPrimitive( QStyle::PE_Frame, &opt, &p, this ); - QPixmap * arrow = m_pressed ? s_arrowSelected : s_arrow; + auto arrow = m_pressed ? m_arrowSelected : m_arrow; - p.drawPixmap( width() - CB_ARROW_BTN_WIDTH + 3, 4, *arrow ); + p.drawPixmap(width() - CB_ARROW_BTN_WIDTH + 3, 4, arrow); if( model() && model()->size() > 0 ) { diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index dcf648c37e3..9ddbc74e187 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -59,11 +59,7 @@ namespace lmms::gui { - SimpleTextFloat * Fader::s_textFloat = nullptr; -QPixmap * Fader::s_back = nullptr; -QPixmap * Fader::s_leds = nullptr; -QPixmap * Fader::s_knob = nullptr; Fader::Fader( FloatModel * _model, const QString & _name, QWidget * _parent ) : QWidget( _parent ), @@ -85,22 +81,6 @@ Fader::Fader( FloatModel * _model, const QString & _name, QWidget * _parent ) : { s_textFloat = new SimpleTextFloat; } - if( ! s_back ) - { - s_back = new QPixmap( embed::getIconPixmap( "fader_background" ) ); - } - if( ! s_leds ) - { - s_leds = new QPixmap( embed::getIconPixmap( "fader_leds" ) ); - } - if( ! s_knob ) - { - s_knob = new QPixmap( embed::getIconPixmap( "fader_knob" ) ); - } - - m_back = s_back; - m_leds = s_leds; - m_knob = s_knob; init(_model, _name); @@ -128,10 +108,6 @@ Fader::Fader( FloatModel * model, const QString & name, QWidget * parent, QPixma s_textFloat = new SimpleTextFloat; } - m_back = back; - m_leds = leds; - m_knob = knob; - init(model, name); } @@ -139,7 +115,7 @@ void Fader::init(FloatModel * model, QString const & name) { setWindowTitle( name ); setAttribute( Qt::WA_OpaquePaintEvent, false ); - QSize backgroundSize = m_back->size(); + QSize backgroundSize = m_back.size(); setMinimumSize( backgroundSize ); setMaximumSize( backgroundSize ); resize( backgroundSize ); @@ -166,7 +142,7 @@ void Fader::mouseMoveEvent( QMouseEvent *mouseEvent ) { int dy = m_moveStartPoint - mouseEvent->globalY(); - float delta = dy * ( model()->maxValue() - model()->minValue() ) / (float) ( height() - ( *m_knob ).height() ); + float delta = dy * (model()->maxValue() - model()->minValue()) / (float)(height() - (m_knob).height()); const auto step = model()->step(); float newValue = static_cast( static_cast( ( m_startValue + delta ) / step + 0.5 ) ) * step; @@ -191,7 +167,7 @@ void Fader::mousePressEvent( QMouseEvent* mouseEvent ) thisModel->saveJournallingState( false ); } - if( mouseEvent->y() >= knobPosY() - ( *m_knob ).height() && mouseEvent->y() < knobPosY() ) + if (mouseEvent->y() >= knobPosY() - (m_knob).height() && mouseEvent->y() < knobPosY()) { updateTextFloat(); s_textFloat->show(); @@ -335,9 +311,9 @@ void Fader::updateTextFloat() inline int Fader::calculateDisplayPeak( float fPeak ) { - int peak = (int)( m_back->height() - ( fPeak / ( m_fMaxPeak - m_fMinPeak ) ) * m_back->height() ); + int peak = static_cast(m_back.height() - (fPeak / (m_fMaxPeak - m_fMinPeak)) * m_back.height()); - return qMin( peak, m_back->height() ); + return qMin(peak, m_back.height()); } @@ -346,7 +322,7 @@ void Fader::paintEvent( QPaintEvent * ev) QPainter painter(this); // Draw the background - painter.drawPixmap( ev->rect(), *m_back, ev->rect() ); + painter.drawPixmap(ev->rect(), m_back, ev->rect()); // Draw the levels with peaks if (getLevelsDisplayedInDBFS()) @@ -359,14 +335,14 @@ void Fader::paintEvent( QPaintEvent * ev) } // Draw the knob - painter.drawPixmap( 0, knobPosY() - m_knob->height(), *m_knob ); + painter.drawPixmap(0, knobPosY() - m_knob.height(), m_knob); } void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) { - int height = m_back->height(); - int width = m_back->width() / 2; - int center = m_back->width() - width; + int height = m_back.height(); + int width = m_back.width() / 2; + int center = m_back.width() - width; float const maxDB(ampToDbfs(m_fMaxPeak)); float const minDB(ampToDbfs(m_fMinPeak)); @@ -380,7 +356,7 @@ void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) float const leftSpan = ampToDbfs(qMax(0.0001, m_fPeakValue_L)) - minDB; int peak_L = height * leftSpan * fullSpanReciprocal; QRect drawRectL( 0, height - peak_L, width, peak_L ); // Source and target are identical - painter.drawPixmap( drawRectL, *m_leds, drawRectL ); + painter.drawPixmap(drawRectL, m_leds, drawRectL); float const persistentLeftPeakDBFS = ampToDbfs(qMax(0.0001, m_persistentPeak_L)); int persistentPeak_L = height * (1 - (persistentLeftPeakDBFS - minDB) * fullSpanReciprocal); @@ -402,7 +378,7 @@ void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) float const rightSpan = ampToDbfs(qMax(0.0001, m_fPeakValue_R)) - minDB; int peak_R = height * rightSpan * fullSpanReciprocal; QRect const drawRectR( center, height - peak_R, width, peak_R ); // Source and target are identical - painter.drawPixmap( drawRectR, *m_leds, drawRectR ); + painter.drawPixmap(drawRectR, m_leds, drawRectR); float const persistentRightPeakDBFS = ampToDbfs(qMax(0.0001, m_persistentPeak_R)); int persistentPeak_R = height * (1 - (persistentRightPeakDBFS - minDB) * fullSpanReciprocal); @@ -425,13 +401,13 @@ void Fader::paintLinearLevels(QPaintEvent * ev, QPainter & painter) // peak leds //float fRange = abs( m_fMaxPeak ) + abs( m_fMinPeak ); - int height = m_back->height(); - int width = m_back->width() / 2; - int center = m_back->width() - width; + int height = m_back.height(); + int width = m_back.width() / 2; + int center = m_back.width() - width; int peak_L = calculateDisplayPeak( m_fPeakValue_L - m_fMinPeak ); int persistentPeak_L = qMax( 3, calculateDisplayPeak( m_persistentPeak_L - m_fMinPeak ) ); - painter.drawPixmap( QRect( 0, peak_L, width, height - peak_L ), *m_leds, QRect( 0, peak_L, width, height - peak_L ) ); + painter.drawPixmap(QRect(0, peak_L, width, height - peak_L), m_leds, QRect(0, peak_L, width, height - peak_L)); if( m_persistentPeak_L > 0.05 ) { @@ -442,7 +418,7 @@ void Fader::paintLinearLevels(QPaintEvent * ev, QPainter & painter) int peak_R = calculateDisplayPeak( m_fPeakValue_R - m_fMinPeak ); int persistentPeak_R = qMax( 3, calculateDisplayPeak( m_persistentPeak_R - m_fMinPeak ) ); - painter.drawPixmap( QRect( center, peak_R, width, height - peak_R ), *m_leds, QRect( center, peak_R, width, height - peak_R ) ); + painter.drawPixmap(QRect(center, peak_R, width, height - peak_R), m_leds, QRect(center, peak_R, width, height - peak_R)); if( m_persistentPeak_R > 0.05 ) { diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp new file mode 100644 index 00000000000..7421908e2d2 --- /dev/null +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -0,0 +1,464 @@ +/* + * FloatModelEditorBase.cpp - Base editor for float models + * + * Copyright (c) 2004-2014 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * 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 "FloatModelEditorBase.h" + +#include +#include +#include + +#ifndef __USE_XOPEN +#define __USE_XOPEN +#endif + +#include "lmms_math.h" +#include "CaptionMenu.h" +#include "ControllerConnection.h" +#include "GuiApplication.h" +#include "LocaleHelper.h" +#include "MainWindow.h" +#include "ProjectJournal.h" +#include "SimpleTextFloat.h" +#include "StringPairDrag.h" + + +namespace lmms::gui +{ + +SimpleTextFloat * FloatModelEditorBase::s_textFloat = nullptr; + +FloatModelEditorBase::FloatModelEditorBase(DirectionOfManipulation directionOfManipulation, QWidget * parent, const QString & name) : + QWidget(parent), + FloatModelView(new FloatModel(0, 0, 0, 1, nullptr, name, true), this), + m_volumeKnob(false), + m_volumeRatio(100.0, 0.0, 1000000.0), + m_buttonPressed(false), + m_directionOfManipulation(directionOfManipulation) +{ + initUi(name); +} + + +void FloatModelEditorBase::initUi(const QString & name) +{ + if (s_textFloat == nullptr) + { + s_textFloat = new SimpleTextFloat; + } + + setWindowTitle(name); + + setFocusPolicy(Qt::ClickFocus); + + doConnections(); +} + + +void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) +{ + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime); +} + + +float FloatModelEditorBase::getValue(const QPoint & p) +{ + // Find out which direction/coordinate is relevant for this control + int const coordinate = m_directionOfManipulation == DirectionOfManipulation::Vertical ? p.y() : -p.x(); + + // knob value increase is linear to mouse movement + float value = .4f * coordinate; + + // if shift pressed we want slower movement + if (getGUI()->mainWindow()->isShiftPressed()) + { + value /= 4.0f; + value = qBound(-4.0f, value, 4.0f); + } + + return value * pageSize(); +} + + +void FloatModelEditorBase::contextMenuEvent(QContextMenuEvent *) +{ + // for the case, the user clicked right while pressing left mouse- + // button, the context-menu appears while mouse-cursor is still hidden + // and it isn't shown again until user does something which causes + // an QApplication::restoreOverrideCursor()-call... + mouseReleaseEvent(nullptr); + + CaptionMenu contextMenu(model()->displayName(), this); + addDefaultActions(&contextMenu); + contextMenu.addAction(QPixmap(), + model()->isScaleLogarithmic() ? tr("Set linear") : tr("Set logarithmic"), + this, SLOT(toggleScale())); + contextMenu.addSeparator(); + contextMenu.exec(QCursor::pos()); +} + + +void FloatModelEditorBase::toggleScale() +{ + model()->setScaleLogarithmic(! model()->isScaleLogarithmic()); + update(); +} + + +void FloatModelEditorBase::dragEnterEvent(QDragEnterEvent * dee) +{ + StringPairDrag::processDragEnterEvent(dee, "float_value," + "automatable_model"); +} + + +void FloatModelEditorBase::dropEvent(QDropEvent * de) +{ + QString type = StringPairDrag::decodeKey(de); + QString val = StringPairDrag::decodeValue(de); + if (type == "float_value") + { + model()->setValue(LocaleHelper::toFloat(val)); + de->accept(); + } + else if (type == "automatable_model") + { + auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); + if (mod != nullptr) + { + AutomatableModel::linkModels(model(), mod); + mod->setValue(model()->value()); + } + } +} + + +void FloatModelEditorBase::mousePressEvent(QMouseEvent * me) +{ + if (me->button() == Qt::LeftButton && + ! (me->modifiers() & Qt::ControlModifier) && + ! (me->modifiers() & Qt::ShiftModifier)) + { + AutomatableModel *thisModel = model(); + if (thisModel) + { + thisModel->addJournalCheckPoint(); + thisModel->saveJournallingState(false); + } + + const QPoint & p = me->pos(); + m_lastMousePos = p; + m_leftOver = 0.0f; + + emit sliderPressed(); + + showTextFloat(0, 0); + + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, + QPoint(width() + 2, 0)); + s_textFloat->show(); + m_buttonPressed = true; + } + else if (me->button() == Qt::LeftButton && + (me->modifiers() & Qt::ShiftModifier)) + { + new StringPairDrag("float_value", + QString::number(model()->value()), + QPixmap(), this); + } + else + { + FloatModelView::mousePressEvent(me); + } +} + + +void FloatModelEditorBase::mouseMoveEvent(QMouseEvent * me) +{ + if (m_buttonPressed && me->pos() != m_lastMousePos) + { + // knob position is changed depending on last mouse position + setPosition(me->pos() - m_lastMousePos); + emit sliderMoved(model()->value()); + // original position for next time is current position + m_lastMousePos = me->pos(); + } + s_textFloat->setText(displayValue()); + s_textFloat->show(); +} + + +void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event) +{ + if (event && event->button() == Qt::LeftButton) + { + AutomatableModel *thisModel = model(); + if (thisModel) + { + thisModel->restoreJournallingState(); + } + } + + m_buttonPressed = false; + + emit sliderReleased(); + + QApplication::restoreOverrideCursor(); + + s_textFloat->hide(); +} + + +void FloatModelEditorBase::enterEvent(QEvent *event) +{ + showTextFloat(700, 2000); +} + + +void FloatModelEditorBase::leaveEvent(QEvent *event) +{ + s_textFloat->hide(); +} + + +void FloatModelEditorBase::focusOutEvent(QFocusEvent * fe) +{ + // make sure we don't loose mouse release event + mouseReleaseEvent(nullptr); + QWidget::focusOutEvent(fe); +} + + +void FloatModelEditorBase::mouseDoubleClickEvent(QMouseEvent *) +{ + enterValue(); +} + + +void FloatModelEditorBase::paintEvent(QPaintEvent *) +{ + QPainter p(this); + + QColor const foreground(3, 94, 97); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + // Compute the percentage + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + QRect r = rect(); + p.setPen(foreground); + p.setBrush(foreground); + p.drawRect(QRect(r.topLeft(), QPoint(r.width() * percentage, r.height()))); +} + + +void FloatModelEditorBase::wheelEvent(QWheelEvent * we) +{ + we->accept(); + const int deltaY = we->angleDelta().y(); + float direction = deltaY > 0 ? 1 : -1; + + auto * m = model(); + float const step = m->step(); + float const range = m->range(); + + // This is the default number of steps or mouse wheel events that it takes to sweep + // from the lowest value to the highest value. + // It might be modified if the user presses modifier keys. See below. + float numberOfStepsForFullSweep = 100.; + + auto const modKeys = we->modifiers(); + if (modKeys == Qt::ShiftModifier) + { + // The shift is intended to go through the values in very coarse steps as in: + // "Shift into overdrive" + numberOfStepsForFullSweep = 10; + } + else if (modKeys == Qt::ControlModifier) + { + // The control key gives more control, i.e. it enables more fine-grained adjustments + numberOfStepsForFullSweep = 1000; + } + else if (modKeys == Qt::AltModifier) + { + // The alt key enables even finer adjustments + numberOfStepsForFullSweep = 2000; + + // It seems that on some systems pressing Alt with mess with the directions, + // i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel + // left and right. Account for this quirk. + if (deltaY == 0) + { + int const deltaX = we->angleDelta().x(); + if (deltaX != 0) + { + direction = deltaX > 0 ? 1 : -1; + } + } + } + + // Compute the number of steps but make sure that we always do at least one step + const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); + const int inc = direction * stepMult; + model()->incValue(inc); + + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->setVisibilityTimeOut(1000); + + emit sliderMoved(model()->value()); +} + + +void FloatModelEditorBase::setPosition(const QPoint & p) +{ + const float value = getValue(p) + m_leftOver; + const auto step = model()->step(); + const float oldValue = model()->value(); + + if (model()->isScaleLogarithmic()) // logarithmic code + { + const float pos = model()->minValue() < 0 + ? oldValue / qMax(qAbs(model()->maxValue()), qAbs(model()->minValue())) + : (oldValue - model()->minValue()) / model()->range(); + const float ratio = 0.1f + qAbs(pos) * 15.f; + float newValue = value * ratio; + if (qAbs(newValue) >= step) + { + float roundedValue = qRound((oldValue - value) / step) * step; + model()->setValue(roundedValue); + m_leftOver = 0.0f; + } + else + { + m_leftOver = value; + } + } + + else // linear code + { + if (qAbs(value) >= step) + { + float roundedValue = qRound((oldValue - value) / step) * step; + model()->setValue(roundedValue); + m_leftOver = 0.0f; + } + else + { + m_leftOver = value; + } + } +} + + +void FloatModelEditorBase::enterValue() +{ + bool ok; + float new_val; + + if (isVolumeKnob() && + ConfigManager::inst()->value("app", "displaydbfs").toInt()) + { + new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "-96.0 dBFS and 6.0 dBFS:"), + ampToDbfs(model()->getRoundedValue() / 100.0), + -96.0, 6.0, model()->getDigitCount(), &ok); + if (new_val <= -96.0) + { + new_val = 0.0f; + } + else + { + new_val = dbfsToAmp(new_val) * 100.0; + } + } + else + { + new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "%1 and %2:"). + arg(model()->minValue()). + arg(model()->maxValue()), + model()->getRoundedValue(), + model()->minValue(), + model()->maxValue(), model()->getDigitCount(), &ok); + } + + if (ok) + { + model()->setValue(new_val); + } +} + + +void FloatModelEditorBase::friendlyUpdate() +{ + if (model() && (model()->controllerConnection() == nullptr || + model()->controllerConnection()->getController()->frequentUpdates() == false || + Controller::runningFrames() % (256*4) == 0)) + { + update(); + } +} + + +QString FloatModelEditorBase::displayValue() const +{ + if (isVolumeKnob() && + ConfigManager::inst()->value("app", "displaydbfs").toInt()) + { + return m_description.trimmed() + QString(" %1 dBFS"). + arg(ampToDbfs(model()->getRoundedValue() / volumeRatio()), + 3, 'f', 2); + } + + return m_description.trimmed() + QString(" %1"). + arg(model()->getRoundedValue()) + m_unit; +} + + +void FloatModelEditorBase::doConnections() +{ + if (model() != nullptr) + { + QObject::connect(model(), SIGNAL(dataChanged()), + this, SLOT(friendlyUpdate())); + + QObject::connect(model(), SIGNAL(propertiesChanged()), + this, SLOT(update())); + } +} + +} // namespace lmms::gui diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 56cf29345d0..00a9363c87f 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -24,11 +24,6 @@ #include "Knob.h" -#include -#include -#include -#include -#include #include #ifndef __USE_XOPEN @@ -36,36 +31,19 @@ #endif #include "lmms_math.h" -#include "CaptionMenu.h" -#include "ConfigManager.h" -#include "ControllerConnection.h" #include "DeprecationHelper.h" #include "embed.h" #include "gui_templates.h" -#include "GuiApplication.h" -#include "LocaleHelper.h" -#include "MainWindow.h" -#include "ProjectJournal.h" -#include "SimpleTextFloat.h" -#include "StringPairDrag.h" + namespace lmms::gui { -SimpleTextFloat * Knob::s_textFloat = nullptr; - - - - Knob::Knob( KnobType _knob_num, QWidget * _parent, const QString & _name ) : - QWidget( _parent ), - FloatModelView( new FloatModel( 0, 0, 0, 1, nullptr, _name, true ), this ), + FloatModelEditorBase(DirectionOfManipulation::Vertical, _parent, _name), m_label( "" ), m_isHtmlLabel(false), m_tdRenderer(nullptr), - m_volumeKnob( false ), - m_volumeRatio( 100.0, 0.0, 1000000.0 ), - m_buttonPressed( false ), m_angle( -10 ), m_lineWidth( 0 ), m_textColor( 255, 255, 255 ), @@ -84,18 +62,10 @@ Knob::Knob( QWidget * _parent, const QString & _name ) : void Knob::initUi( const QString & _name ) { - if( s_textFloat == nullptr ) - { - s_textFloat = new SimpleTextFloat; - } - - setWindowTitle( _name ); - onKnobNumUpdated(); setTotalAngle( 270.0f ); setInnerRadius( 1.0f ); setOuterRadius( 10.0f ); - setFocusPolicy( Qt::ClickFocus ); // This is a workaround to enable style sheets for knobs which are not styled knobs. // @@ -123,13 +93,9 @@ void Knob::initUi( const QString & _name ) default: break; } - - doConnections(); } - - void Knob::onKnobNumUpdated() { if( m_knobNum != KnobType::Styled ) @@ -484,195 +450,6 @@ 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; - - // knob value increase is linear to mouse movement - value = .4f * _p.y(); - - // if shift pressed we want slower movement - if( getGUI()->mainWindow()->isShiftPressed() ) - { - value /= 4.0f; - value = qBound( -4.0f, value, 4.0f ); - } - return value * pageSize(); -} - - - - -void Knob::contextMenuEvent( QContextMenuEvent * ) -{ - // for the case, the user clicked right while pressing left mouse- - // button, the context-menu appears while mouse-cursor is still hidden - // and it isn't shown again until user does something which causes - // an QApplication::restoreOverrideCursor()-call... - mouseReleaseEvent( nullptr ); - - CaptionMenu contextMenu( model()->displayName(), this ); - addDefaultActions( &contextMenu ); - contextMenu.addAction( QPixmap(), - model()->isScaleLogarithmic() ? tr( "Set linear" ) : tr( "Set logarithmic" ), - this, SLOT(toggleScale())); - contextMenu.addSeparator(); - contextMenu.exec( QCursor::pos() ); -} - - -void Knob::toggleScale() -{ - model()->setScaleLogarithmic( ! model()->isScaleLogarithmic() ); - update(); -} - - - -void Knob::dragEnterEvent( QDragEnterEvent * _dee ) -{ - StringPairDrag::processDragEnterEvent( _dee, "float_value," - "automatable_model" ); -} - - - - -void Knob::dropEvent( QDropEvent * _de ) -{ - QString type = StringPairDrag::decodeKey( _de ); - QString val = StringPairDrag::decodeValue( _de ); - if( type == "float_value" ) - { - model()->setValue( LocaleHelper::toFloat(val) ); - _de->accept(); - } - else if( type == "automatable_model" ) - { - auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); - if( mod != nullptr ) - { - AutomatableModel::linkModels( model(), mod ); - mod->setValue( model()->value() ); - } - } -} - - - - -void Knob::mousePressEvent( QMouseEvent * _me ) -{ - if( _me->button() == Qt::LeftButton && - ! ( _me->modifiers() & Qt::ControlModifier ) && - ! ( _me->modifiers() & Qt::ShiftModifier ) ) - { - AutomatableModel *thisModel = model(); - if( thisModel ) - { - thisModel->addJournalCheckPoint(); - thisModel->saveJournallingState( false ); - } - - const QPoint & p = _me->pos(); - m_lastMousePos = p; - m_leftOver = 0.0f; - - emit sliderPressed(); - - showTextFloat(0, 0); - - m_buttonPressed = true; - } - else if( _me->button() == Qt::LeftButton && - (_me->modifiers() & Qt::ShiftModifier) ) - { - new StringPairDrag( "float_value", - QString::number( model()->value() ), - QPixmap(), this ); - } - else - { - FloatModelView::mousePressEvent( _me ); - } -} - - - - -void Knob::mouseMoveEvent( QMouseEvent * _me ) -{ - if( m_buttonPressed && _me->pos() != m_lastMousePos ) - { - // knob position is changed depending on last mouse position - setPosition( _me->pos() - m_lastMousePos ); - emit sliderMoved( model()->value() ); - // original position for next time is current position - m_lastMousePos = _me->pos(); - } - s_textFloat->setText( displayValue() ); - s_textFloat->show(); -} - - - - -void Knob::mouseReleaseEvent( QMouseEvent* event ) -{ - if( event && event->button() == Qt::LeftButton ) - { - AutomatableModel *thisModel = model(); - if( thisModel ) - { - thisModel->restoreJournallingState(); - } - } - - m_buttonPressed = false; - - emit sliderReleased(); - - QApplication::restoreOverrideCursor(); - - s_textFloat->hide(); -} - -void Knob::enterEvent(QEvent *event) -{ - showTextFloat(700, 2000); -} - -void Knob::leaveEvent(QEvent *event) -{ - s_textFloat->hide(); -} - - -void Knob::focusOutEvent( QFocusEvent * _fe ) -{ - // make sure we don't loose mouse release event - mouseReleaseEvent( nullptr ); - QWidget::focusOutEvent( _fe ); -} - - - - -void Knob::mouseDoubleClickEvent( QMouseEvent * ) -{ - enterValue(); -} - - - - void Knob::paintEvent( QPaintEvent * _me ) { QPainter p( this ); @@ -697,201 +474,6 @@ void Knob::paintEvent( QPaintEvent * _me ) } } - - - -void Knob::wheelEvent(QWheelEvent * we) -{ - we->accept(); - const int deltaY = we->angleDelta().y(); - float direction = deltaY > 0 ? 1 : -1; - - auto * m = model(); - float const step = m->step(); - float const range = m->range(); - - // This is the default number of steps or mouse wheel events that it takes to sweep - // from the lowest value to the highest value. - // It might be modified if the user presses modifier keys. See below. - float numberOfStepsForFullSweep = 100.; - - auto const modKeys = we->modifiers(); - if (modKeys == Qt::ShiftModifier) - { - // The shift is intended to go through the values in very coarse steps as in: - // "Shift into overdrive" - numberOfStepsForFullSweep = 10; - } - else if (modKeys == Qt::ControlModifier) - { - // The control key gives more control, i.e. it enables more fine-grained adjustments - numberOfStepsForFullSweep = 1000; - } - else if (modKeys == Qt::AltModifier) - { - // The alt key enables even finer adjustments - numberOfStepsForFullSweep = 2000; - - // It seems that on some systems pressing Alt with mess with the directions, - // i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel - // left and right. Account for this quirk. - if (deltaY == 0) - { - int const deltaX = we->angleDelta().x(); - if (deltaX != 0) - { - direction = deltaX > 0 ? 1 : -1; - } - } - } - - // Compute the number of steps but make sure that we always do at least one step - const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); - const int inc = direction * stepMult; - model()->incValue(inc); - - s_textFloat->setText( displayValue() ); - s_textFloat->moveGlobal( this, QPoint( width() + 2, 0 ) ); - s_textFloat->setVisibilityTimeOut( 1000 ); - - emit sliderMoved( model()->value() ); -} - - - - -void Knob::setPosition( const QPoint & _p ) -{ - const float value = getValue( _p ) + m_leftOver; - const auto step = model()->step(); - const float oldValue = model()->value(); - - - - if( model()->isScaleLogarithmic() ) // logarithmic code - { - const float pos = model()->minValue() < 0 - ? oldValue / qMax( qAbs( model()->maxValue() ), qAbs( model()->minValue() ) ) - : ( oldValue - model()->minValue() ) / model()->range(); - const float ratio = 0.1f + qAbs( pos ) * 15.f; - float newValue = value * ratio; - if( qAbs( newValue ) >= step ) - { - float roundedValue = qRound( ( oldValue - value ) / step ) * step; - model()->setValue( roundedValue ); - m_leftOver = 0.0f; - } - else - { - m_leftOver = value; - } - } - - else // linear code - { - if( qAbs( value ) >= step ) - { - float roundedValue = qRound( ( oldValue - value ) / step ) * step; - model()->setValue( roundedValue ); - m_leftOver = 0.0f; - } - else - { - m_leftOver = value; - } - } -} - - - - -void Knob::enterValue() -{ - bool ok; - float new_val; - - if( isVolumeKnob() && - ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ) - { - new_val = QInputDialog::getDouble( - this, tr( "Set value" ), - tr( "Please enter a new value between " - "-96.0 dBFS and 6.0 dBFS:" ), - ampToDbfs( model()->getRoundedValue() / 100.0 ), - -96.0, 6.0, model()->getDigitCount(), &ok ); - if( new_val <= -96.0 ) - { - new_val = 0.0f; - } - else - { - new_val = dbfsToAmp( new_val ) * 100.0; - } - } - else - { - new_val = QInputDialog::getDouble( - this, tr( "Set value" ), - tr( "Please enter a new value between " - "%1 and %2:" ). - arg( model()->minValue() ). - arg( model()->maxValue() ), - model()->getRoundedValue(), - model()->minValue(), - model()->maxValue(), model()->getDigitCount(), &ok ); - } - - if( ok ) - { - model()->setValue( new_val ); - } -} - - - - -void Knob::friendlyUpdate() -{ - if (model() && (model()->controllerConnection() == nullptr || - model()->controllerConnection()->getController()->frequentUpdates() == false || - Controller::runningFrames() % (256*4) == 0)) - { - update(); - } -} - - - - -QString Knob::displayValue() const -{ - if( isVolumeKnob() && - ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ) - { - return m_description.trimmed() + QString( " %1 dBFS" ). - arg( ampToDbfs( model()->getRoundedValue() / volumeRatio() ), - 3, 'f', 2 ); - } - return m_description.trimmed() + QString( " %1" ). - arg( model()->getRoundedValue() ) + m_unit; -} - - - - -void Knob::doConnections() -{ - if( model() != nullptr ) - { - QObject::connect( model(), SIGNAL(dataChanged()), - this, SLOT(friendlyUpdate())); - - QObject::connect( model(), SIGNAL(propertiesChanged()), - this, SLOT(update())); - } -} - - void Knob::changeEvent(QEvent * ev) { if (ev->type() == QEvent::EnabledChange) diff --git a/src/gui/widgets/LcdFloatSpinBox.cpp b/src/gui/widgets/LcdFloatSpinBox.cpp index 96f2b27e1db..c7e20467a5b 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -109,11 +109,16 @@ void LcdFloatSpinBox::layoutSetup(const QString &style) void LcdFloatSpinBox::update() { - const int whole = static_cast(model()->value()); - const float fraction = model()->value() - whole; - const int intFraction = fraction * std::pow(10.f, m_fractionDisplay.numDigits()); - m_wholeDisplay.setValue(whole); - m_fractionDisplay.setValue(intFraction); + const int digitValue = std::pow(10.f, m_fractionDisplay.numDigits()); + float value = model()->value(); + int fraction = std::abs(std::round((value - static_cast(value)) * digitValue)); + if (fraction == digitValue) + { + value += std::copysign(1, value); + fraction = 0; + } + m_wholeDisplay.setValue(value); + m_fractionDisplay.setValue(fraction); QWidget::update(); } @@ -129,6 +134,9 @@ void LcdFloatSpinBox::contextMenuEvent(QContextMenuEvent* event) void LcdFloatSpinBox::mousePressEvent(QMouseEvent* event) { + // switch between integer and fractional step based on cursor position + m_intStep = event->x() < m_wholeDisplay.width(); + if (event->button() == Qt::LeftButton && !(event->modifiers() & Qt::ControlModifier) && event->y() < m_wholeDisplay.cellHeight() + 2) @@ -152,10 +160,6 @@ void LcdFloatSpinBox::mousePressEvent(QMouseEvent* event) void LcdFloatSpinBox::mouseMoveEvent(QMouseEvent* event) { - // switch between integer and fractional step based on cursor position - if (event->x() < m_wholeDisplay.width()) { m_intStep = true; } - else { m_intStep = false; } - if (m_mouseMoving) { int dy = event->globalY() - m_origMousePos.y(); diff --git a/src/gui/widgets/LcdWidget.cpp b/src/gui/widgets/LcdWidget.cpp index 0f5e1346658..a409fee8bf6 100644 --- a/src/gui/widgets/LcdWidget.cpp +++ b/src/gui/widgets/LcdWidget.cpp @@ -66,17 +66,6 @@ LcdWidget::LcdWidget(int numDigits, const QString& style, QWidget* parent, const initUi( name, style ); } - - - -LcdWidget::~LcdWidget() -{ - delete m_lcdPixmap; -} - - - - void LcdWidget::setValue(int value) { QString s = m_textForValue[value]; @@ -94,6 +83,22 @@ void LcdWidget::setValue(int value) update(); } +void LcdWidget::setValue(float value) +{ + if (value < 0 && value > -1) + { + QString s = QString::number(static_cast(value)); + s.prepend('-'); + + m_display = s; + update(); + } + else + { + setValue(static_cast(value)); + } +} + @@ -146,11 +151,8 @@ void LcdWidget::paintEvent( QPaintEvent* ) { p.translate(margin, margin); // Left Margin - p.drawPixmap( - cellRect, - *m_lcdPixmap, - QRect(QPoint(charsPerPixmap * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize) - ); + p.drawPixmap(cellRect, m_lcdPixmap, + QRect(QPoint(charsPerPixmap * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize)); p.translate(m_marginWidth, 0); } @@ -158,8 +160,7 @@ void LcdWidget::paintEvent( QPaintEvent* ) // Padding for( int i=0; i < m_numDigits - m_display.length(); i++ ) { - p.drawPixmap( cellRect, *m_lcdPixmap, - QRect( QPoint( 10 * m_cellWidth, isEnabled()?0:m_cellHeight) , cellSize ) ); + p.drawPixmap(cellRect, m_lcdPixmap, QRect(QPoint(10 * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize)); p.translate( m_cellWidth, 0 ); } @@ -173,19 +174,14 @@ void LcdWidget::paintEvent( QPaintEvent* ) else val = 10; } - p.drawPixmap( cellRect, *m_lcdPixmap, - QRect( QPoint( val*m_cellWidth, - isEnabled()?0:m_cellHeight ), - cellSize ) ); + p.drawPixmap(cellRect, m_lcdPixmap, QRect(QPoint(val * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize)); p.translate( m_cellWidth, 0 ); } // Right Margin - p.drawPixmap(QRect(0, 0, m_seamlessRight ? 0 : m_marginWidth - 1, m_cellHeight), - *m_lcdPixmap, + p.drawPixmap(QRect(0, 0, m_seamlessRight ? 0 : m_marginWidth - 1, m_cellHeight), m_lcdPixmap, QRect(charsPerPixmap * m_cellWidth, isEnabled() ? 0 : m_cellHeight, m_cellWidth / 2, m_cellHeight)); - p.restore(); // Border @@ -278,12 +274,12 @@ void LcdWidget::initUi(const QString& name , const QString& style) setWindowTitle( name ); // We should make a factory for these or something. - //m_lcdPixmap = new QPixmap( embed::getIconPixmap( QString( "lcd_" + style ).toUtf8().constData() ) ); - //m_lcdPixmap = new QPixmap( embed::getIconPixmap( "lcd_19green" ) ); // TODO!! - m_lcdPixmap = new QPixmap( embed::getIconPixmap( QString( "lcd_" + style ).toUtf8().constData() ) ); + //m_lcdPixmap = embed::getIconPixmap(QString("lcd_" + style).toUtf8().constData()); + //m_lcdPixmap = embed::getIconPixmap("lcd_19green"); // TODO!! - m_cellWidth = m_lcdPixmap->size().width() / LcdWidget::charsPerPixmap; - m_cellHeight = m_lcdPixmap->size().height() / 2; + m_lcdPixmap = embed::getIconPixmap(QString("lcd_" + style).toUtf8().constData()); + m_cellWidth = m_lcdPixmap.size().width() / LcdWidget::charsPerPixmap; + m_cellHeight = m_lcdPixmap.size().height() / 2; m_marginWidth = m_cellWidth / 2; diff --git a/src/gui/widgets/LedCheckBox.cpp b/src/gui/widgets/LedCheckBox.cpp index 0c16bf391ae..1dbf650ed6e 100644 --- a/src/gui/widgets/LedCheckBox.cpp +++ b/src/gui/widgets/LedCheckBox.cpp @@ -44,9 +44,10 @@ static const auto names = std::array LedCheckBox::LedCheckBox( const QString & _text, QWidget * _parent, - const QString & _name, LedColor _color ) : + const QString & _name, LedColor _color, bool legacyMode ) : AutomatableButton( _parent, _name ), - m_text( _text ) + m_text( _text ), + m_legacyMode(legacyMode) { initUi( _color ); } @@ -55,22 +56,11 @@ LedCheckBox::LedCheckBox( const QString & _text, QWidget * _parent, LedCheckBox::LedCheckBox( QWidget * _parent, - const QString & _name, LedColor _color ) : - LedCheckBox( QString(), _parent, _name, _color ) + const QString & _name, LedColor _color, bool legacyMode ) : + LedCheckBox( QString(), _parent, _name, _color, legacyMode ) { } - - -LedCheckBox::~LedCheckBox() -{ - delete m_ledOnPixmap; - delete m_ledOffPixmap; -} - - - - void LedCheckBox::setText( const QString &s ) { m_text = s; @@ -80,24 +70,16 @@ void LedCheckBox::setText( const QString &s ) -void LedCheckBox::paintEvent( QPaintEvent * ) +void LedCheckBox::paintEvent( QPaintEvent * pe ) { - QPainter p( this ); - p.setFont( pointSize<7>( font() ) ); - - if( model()->value() == true ) - { - p.drawPixmap( 0, 0, *m_ledOnPixmap ); + if (!m_legacyMode) + { + paintNonLegacy(pe); } else { - p.drawPixmap( 0, 0, *m_ledOffPixmap ); + paintLegacy(pe); } - - p.setPen( QColor( 64, 64, 64 ) ); - p.drawText( m_ledOffPixmap->width() + 4, 11, text() ); - p.setPen( QColor( 255, 255, 255 ) ); - p.drawText( m_ledOffPixmap->width() + 3, 10, text() ); } @@ -107,11 +89,14 @@ void LedCheckBox::initUi( LedColor _color ) { setCheckable( true ); - m_ledOnPixmap = new QPixmap( embed::getIconPixmap( - names[static_cast(_color)].toUtf8().constData() ) ); - m_ledOffPixmap = new QPixmap( embed::getIconPixmap( "led_off" ) ); + m_ledOnPixmap = embed::getIconPixmap(names[static_cast(_color)].toUtf8().constData()); + m_ledOffPixmap = embed::getIconPixmap("led_off"); + + if (m_legacyMode) + { + setFont( pointSize<7>( font() ) ); + } - setFont( pointSize<7>( font() ) ); setText( m_text ); } @@ -120,9 +105,38 @@ void LedCheckBox::initUi( LedColor _color ) void LedCheckBox::onTextUpdated() { - setFixedSize(m_ledOffPixmap->width() + 5 + horizontalAdvance(QFontMetrics(font()), - text()), - m_ledOffPixmap->height()); + QFontMetrics const fm = fontMetrics(); + + int const width = m_ledOffPixmap.width() + 5 + horizontalAdvance(fm, text()); + int const height = m_legacyMode ? m_ledOffPixmap.height() : qMax(m_ledOffPixmap.height(), fm.height()); + + setFixedSize(width, height); +} + +void LedCheckBox::paintLegacy(QPaintEvent * pe) +{ + QPainter p( this ); + p.setFont( pointSize<7>( font() ) ); + + p.drawPixmap(0, 0, model()->value() ? m_ledOnPixmap : m_ledOffPixmap); + + p.setPen( QColor( 64, 64, 64 ) ); + p.drawText(m_ledOffPixmap.width() + 4, 11, text()); + p.setPen( QColor( 255, 255, 255 ) ); + p.drawText(m_ledOffPixmap.width() + 3, 10, text()); +} + +void LedCheckBox::paintNonLegacy(QPaintEvent * pe) +{ + QPainter p(this); + + auto drawnPixmap = model()->value() ? m_ledOnPixmap : m_ledOffPixmap; + + p.drawPixmap(0, rect().height() / 2 - drawnPixmap.height() / 2, drawnPixmap); + + QRect r = rect(); + r -= QMargins(m_ledOffPixmap.width() + 5, 0, 0, 0); + p.drawText(r, text()); } diff --git a/src/gui/widgets/TabBar.cpp b/src/gui/widgets/TabBar.cpp index 806a932528c..e2949455138 100644 --- a/src/gui/widgets/TabBar.cpp +++ b/src/gui/widgets/TabBar.cpp @@ -44,7 +44,7 @@ TabBar::TabBar( QWidget * _parent, QBoxLayout::Direction _dir ) : } TabButton * TabBar::addTab( QWidget * _w, const QString & _text, int _id, - bool _add_stretch, bool _text_is_tooltip ) + bool _add_stretch, bool _text_is_tooltip, bool fixWidgetToParentSize ) { // already tab with id? if( m_tabs.contains( _id ) ) @@ -83,10 +83,12 @@ TabButton * TabBar::addTab( QWidget * _w, const QString & _text, int _id, m_layout->addStretch(); } - - // we assume, parent-widget is a widget acting as widget-stack so all - // widgets have the same size and only the one on the top is visible - _w->setFixedSize( _w->parentWidget()->size() ); + if (fixWidgetToParentSize) + { + // we assume, parent-widget is a widget acting as widget-stack so all + // widgets have the same size and only the one on the top is visible + _w->setFixedSize( _w->parentWidget()->size() ); + } b->setFont( pointSize<8>( b->font() ) ); diff --git a/src/gui/widgets/TempoSyncBarModelEditor.cpp b/src/gui/widgets/TempoSyncBarModelEditor.cpp new file mode 100644 index 00000000000..5ff2332e051 --- /dev/null +++ b/src/gui/widgets/TempoSyncBarModelEditor.cpp @@ -0,0 +1,302 @@ +/* + * TempoSyncBarModelEditor.cpp - adds bpm to ms conversion for the bar editor class + * + * Copyright (c) 2005-2007 Danny McRae + * Copyright (c) 2005-2009 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * 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 + +#include "TempoSyncBarModelEditor.h" +#include "Engine.h" +#include "CaptionMenu.h" +#include "embed.h" +#include "GuiApplication.h" +#include "MainWindow.h" +#include "MeterDialog.h" +#include "Song.h" +#include "SubWindow.h" + + +namespace lmms::gui +{ + +TempoSyncBarModelEditor::TempoSyncBarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + BarModelEditor(text, floatModel, parent), + m_tempoSyncIcon(embed::getIconPixmap("tempo_sync")), + m_tempoSyncDescription(tr("Tempo Sync")), + m_custom(nullptr) +{ + modelChanged(); +} + + +TempoSyncBarModelEditor::~TempoSyncBarModelEditor() +{ + if(m_custom) + { + delete m_custom->parentWidget(); + } +} + + +void TempoSyncBarModelEditor::modelChanged() +{ + TempoSyncKnobModel * tempoSyncModel = model(); + + if(tempoSyncModel == nullptr) + { + qWarning("no TempoSyncKnobModel has been set!"); + } + + if(m_custom != nullptr) + { + m_custom->setModel(&tempoSyncModel->getCustomMeterModel()); + } + + connect(tempoSyncModel, &TempoSyncKnobModel::syncModeChanged, this, &TempoSyncBarModelEditor::updateDescAndIcon); + connect(this, SIGNAL(sliderMoved(float)), tempoSyncModel, SLOT(disableSync())); + + updateDescAndIcon(); +} + + +void TempoSyncBarModelEditor::contextMenuEvent(QContextMenuEvent *) +{ + mouseReleaseEvent(nullptr); + + TempoSyncKnobModel * tempoSyncModel = model(); + + CaptionMenu contextMenu(tempoSyncModel->displayName(), this); + addDefaultActions(&contextMenu); + + contextMenu.addSeparator(); + + float limit = 60000.0f / (Engine::getSong()->getTempo() * tempoSyncModel->scale()); + + QMenu * syncMenu = contextMenu.addMenu(m_tempoSyncIcon, m_tempoSyncDescription); + + float const maxValue = tempoSyncModel->maxValue(); + + if(limit / 8.0f <= maxValue) + { + connect(syncMenu, SIGNAL(triggered(QAction*)), tempoSyncModel, SLOT(setTempoSync(QAction*))); + + syncMenu->addAction(embed::getIconPixmap("note_none"), + tr("No Sync"))->setData((int) TempoSyncKnobModel::SyncMode::None); + + if(limit / 0.125f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_double_whole"), + tr("Eight beats"))->setData((int) TempoSyncKnobModel::SyncMode::DoubleWholeNote); + } + + if(limit / 0.25f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_whole"), + tr("Whole note"))->setData((int) TempoSyncKnobModel::SyncMode::WholeNote); + } + + if(limit / 0.5f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_half"), + tr("Half note"))->setData((int) TempoSyncKnobModel::SyncMode::HalfNote); + } + + if(limit <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_quarter"), + tr("Quarter note"))->setData((int) TempoSyncKnobModel::SyncMode::QuarterNote); + } + + if(limit / 2.0f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_eighth"), + tr("8th note"))->setData((int) TempoSyncKnobModel::SyncMode::EighthNote); + } + + if(limit / 4.0f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_sixteenth"), + tr("16th note"))->setData((int) TempoSyncKnobModel::SyncMode::SixteenthNote); + } + + syncMenu->addAction(embed::getIconPixmap("note_thirtysecond"), + tr("32nd note"))->setData((int) TempoSyncKnobModel::SyncMode::ThirtysecondNote); + + syncMenu->addAction(embed::getIconPixmap("dont_know"), + tr("Custom..."), this, SLOT(showCustom()))->setData((int) TempoSyncKnobModel::SyncMode::Custom); + + contextMenu.addSeparator(); + } + + contextMenu.exec(QCursor::pos()); + + delete syncMenu; +} + +void TempoSyncBarModelEditor::updateDescAndIcon() +{ + updateTextDescription(); + + if(m_custom != nullptr && model()->syncMode() != TempoSyncKnobModel::SyncMode::Custom) + { + m_custom->parentWidget()->hide(); + } + + updateIcon(); + + emit syncDescriptionChanged(m_tempoSyncDescription); + emit syncIconChanged(); +} + + +const QString & TempoSyncBarModelEditor::syncDescription() +{ + return m_tempoSyncDescription; +} + + +void TempoSyncBarModelEditor::setSyncDescription(const QString & new_description) +{ + m_tempoSyncDescription = new_description; + emit syncDescriptionChanged(new_description); +} + + +const QPixmap & TempoSyncBarModelEditor::syncIcon() +{ + return m_tempoSyncIcon; +} + + +void TempoSyncBarModelEditor::setSyncIcon(const QPixmap & new_icon) +{ + m_tempoSyncIcon = new_icon; + emit syncIconChanged(); +} + + +void TempoSyncBarModelEditor::showCustom() +{ + if(m_custom == nullptr) + { + m_custom = new MeterDialog(getGUI()->mainWindow()->workspace()); + QMdiSubWindow * subWindow = getGUI()->mainWindow()->addWindowedWidget(m_custom); + Qt::WindowFlags flags = subWindow->windowFlags(); + flags &= ~Qt::WindowMaximizeButtonHint; + subWindow->setWindowFlags(flags); + subWindow->setFixedSize(subWindow->size()); + m_custom->setWindowTitle("Meter"); + m_custom->setModel(&model()->getCustomMeterModel()); + } + + m_custom->parentWidget()->show(); + model()->setTempoSync(TempoSyncKnobModel::SyncMode::Custom); +} + + +void TempoSyncBarModelEditor::updateTextDescription() +{ + TempoSyncKnobModel * tempoSyncModel = model(); + + auto const syncMode = tempoSyncModel->syncMode(); + + switch(syncMode) + { + case TempoSyncKnobModel::SyncMode::None: + m_tempoSyncDescription = tr("Tempo Sync"); + break; + case TempoSyncKnobModel::SyncMode::Custom: + m_tempoSyncDescription = tr("Custom ") + + "(" + + QString::number(tempoSyncModel->getCustomMeterModel().numeratorModel().value()) + + "/" + + QString::number(tempoSyncModel->getCustomMeterModel().denominatorModel().value()) + + ")"; + break; + case TempoSyncKnobModel::SyncMode::DoubleWholeNote: + m_tempoSyncDescription = tr("Synced to Eight Beats"); + break; + case TempoSyncKnobModel::SyncMode::WholeNote: + m_tempoSyncDescription = tr("Synced to Whole Note"); + break; + case TempoSyncKnobModel::SyncMode::HalfNote: + m_tempoSyncDescription = tr("Synced to Half Note"); + break; + case TempoSyncKnobModel::SyncMode::QuarterNote: + m_tempoSyncDescription = tr("Synced to Quarter Note"); + break; + case TempoSyncKnobModel::SyncMode::EighthNote: + m_tempoSyncDescription = tr("Synced to 8th Note"); + break; + case TempoSyncKnobModel::SyncMode::SixteenthNote: + m_tempoSyncDescription = tr("Synced to 16th Note"); + break; + case TempoSyncKnobModel::SyncMode::ThirtysecondNote: + m_tempoSyncDescription = tr("Synced to 32nd Note"); + break; + default: ; + } +} + +void TempoSyncBarModelEditor::updateIcon() +{ + switch(model()->syncMode()) + { + case TempoSyncKnobModel::SyncMode::None: + m_tempoSyncIcon = embed::getIconPixmap("tempo_sync"); + break; + case TempoSyncKnobModel::SyncMode::Custom: + m_tempoSyncIcon = embed::getIconPixmap("dont_know"); + break; + case TempoSyncKnobModel::SyncMode::DoubleWholeNote: + m_tempoSyncIcon = embed::getIconPixmap("note_double_whole"); + break; + case TempoSyncKnobModel::SyncMode::WholeNote: + m_tempoSyncIcon = embed::getIconPixmap("note_whole"); + break; + case TempoSyncKnobModel::SyncMode::HalfNote: + m_tempoSyncIcon = embed::getIconPixmap("note_half"); + break; + case TempoSyncKnobModel::SyncMode::QuarterNote: + m_tempoSyncIcon = embed::getIconPixmap("note_quarter"); + break; + case TempoSyncKnobModel::SyncMode::EighthNote: + m_tempoSyncIcon = embed::getIconPixmap("note_eighth"); + break; + case TempoSyncKnobModel::SyncMode::SixteenthNote: + m_tempoSyncIcon = embed::getIconPixmap("note_sixteenth"); + break; + case TempoSyncKnobModel::SyncMode::ThirtysecondNote: + m_tempoSyncIcon = embed::getIconPixmap("note_thirtysecond"); + break; + default: + qWarning("TempoSyncKnob::calculateTempoSyncTime:" + "invalid TempoSyncMode"); + break; + } +} + + +} // namespace lmms::gui diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index d130d6fc255..89db21a7bfb 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -23,6 +23,7 @@ #cmakedefine LMMS_HAVE_LV2 #cmakedefine LMMS_HAVE_SUIL #cmakedefine LMMS_HAVE_MP3LAME +#cmakedefine LMMS_HAVE_SNDFILE_MP3 #cmakedefine LMMS_HAVE_OGGVORBIS #cmakedefine LMMS_HAVE_OSS #cmakedefine LMMS_HAVE_SNDIO diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 29fda075e9c..4b00e0d79e6 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -727,7 +727,8 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, // Handle automation: detuning for (const auto& processHandle : m_processHandles) { - processHandle->processTimePos(_start, m_pitchModel.value(), gui::GuiApplication::instance()->pianoRoll()->isRecording()); + processHandle->processTimePos( + _start, m_pitchModel.value(), gui::getGUI() && gui::getGUI()->pianoRoll()->isRecording()); } if ( clips.size() == 0 ) @@ -775,8 +776,11 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, while( nit != notes.end() && ( cur_note = *nit )->pos() == cur_start ) { - const f_cnt_t note_frames = - cur_note->length().frames( frames_per_tick ); + // If the note is a Step Note, frames will be 0 so the NotePlayHandle + // plays for the whole length of the sample + const auto note_frames = cur_note->type() == Note::Type::Step + ? 0 + : cur_note->length().frames(frames_per_tick); NotePlayHandle* notePlayHandle = NotePlayHandleManager::acquire( this, _offset, note_frames, *cur_note ); notePlayHandle->setPatternTrack(pattern_track); diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index 490f6e6d041..b5e764b177f 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -25,6 +25,7 @@ #include "MidiClip.h" +#include #include #include "GuiApplication.h" @@ -38,13 +39,6 @@ namespace lmms { -QPixmap * gui::MidiClipView::s_stepBtnOn0 = nullptr; -QPixmap * gui::MidiClipView::s_stepBtnOn200 = nullptr; -QPixmap * gui::MidiClipView::s_stepBtnOff = nullptr; -QPixmap * gui::MidiClipView::s_stepBtnOffLight = nullptr; - - - MidiClip::MidiClip( InstrumentTrack * _instrument_track ) : Clip( _instrument_track ), m_instrumentTrack( _instrument_track ), @@ -174,19 +168,18 @@ TimePos MidiClip::beatClipLength() const for (const auto& note : m_notes) { - if (note->length() < 0) + if (note->type() == Note::Type::Step) { max_length = std::max(max_length, note->pos() + 1); } } - if( m_steps != TimePos::stepsPerBar() ) + if (m_steps != TimePos::stepsPerBar()) { - max_length = m_steps * TimePos::ticksPerBar() / - TimePos::stepsPerBar(); + max_length = m_steps * TimePos::ticksPerBar() / TimePos::stepsPerBar(); } - return TimePos( max_length ).nextFullBar() * TimePos::ticksPerBar(); + return TimePos{max_length}.nextFullBar() * TimePos::ticksPerBar(); } @@ -235,13 +228,13 @@ void MidiClip::removeNote( Note * _note_to_del ) } -// returns a pointer to the note at specified step, or NULL if note doesn't exist - -Note * MidiClip::noteAtStep( int _step ) +// Returns a pointer to the note at specified step, or nullptr if note doesn't exist +Note * MidiClip::noteAtStep(int step) { for (const auto& note : m_notes) { - if (note->pos() == TimePos::stepPosition(_step) && note->length() < 0) + if (note->pos() == TimePos::stepPosition(step) + && note->type() == Note::Type::Step) { return note; } @@ -278,8 +271,10 @@ void MidiClip::clearNotes() Note * MidiClip::addStepNote( int step ) { - return addNote( Note( TimePos( -DefaultTicksPerBar ), - TimePos::stepPosition( step ) ), false ); + Note stepNote = Note(TimePos(DefaultTicksPerBar / 16), TimePos::stepPosition(step)); + stepNote.setType(Note::Type::Step); + + return addNote(stepNote, false); } @@ -351,15 +346,10 @@ void MidiClip::setType( Type _new_clip_type ) void MidiClip::checkType() { - for (auto& note : m_notes) - { - if (note->length() > 0) - { - setType(Type::MelodyClip); - return; - } - } - setType( Type::BeatClip ); + // If all notes are StepNotes, we have a BeatClip + const auto beatClip = std::all_of(m_notes.begin(), m_notes.end(), [](auto note) { return note->type() == Note::Type::Step; }); + + setType(beatClip ? Type::BeatClip : Type::MelodyClip); } @@ -370,9 +360,9 @@ void MidiClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "type", static_cast(m_clipType) ); _this.setAttribute( "name", name() ); - if( usesCustomClipColor() ) + if (const auto& c = color()) { - _this.setAttribute( "color", color().name() ); + _this.setAttribute("color", c->name()); } // as the target of copied/dragged MIDI clip is always an existing // MIDI clip, we must not store actual position, instead we store -1 @@ -404,17 +394,12 @@ void MidiClip::loadSettings( const QDomElement & _this ) m_clipType = static_cast( _this.attribute( "type" ).toInt() ); setName( _this.attribute( "name" ) ); - - if( _this.hasAttribute( "color" ) ) - { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); - } - else + + if (_this.hasAttribute("color")) { - useCustomClipColor(false); + setColor(QColor{_this.attribute("color")}); } - + if( _this.attribute( "pos" ).toInt() >= 0 ) { movePosition( _this.attribute( "pos" ).toInt() ); diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000000..48a3e3c28c3 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + { + "name": "fftw3", + "default-features": false, + "features": [ + "sse", + "sse2", + "avx", + "avx2" + ] + }, + { + "name": "fltk", + "default-features": false + }, + { + "name": "fluidsynth", + "default-features": false, + "features": [ + "sndfile" + ] + }, + { + "name": "libogg", + "default-features": false + }, + { + "name": "libsamplerate", + "default-features": false + }, + { + "name": "libsndfile", + "default-features": false, + "features": [ + "external-libs", + "mpeg" + ] + }, + { + "name": "libstk", + "default-features": false + }, + { + "name": "libvorbis", + "default-features": false + }, + { + "name": "lilv", + "default-features": false + }, + { + "name": "lv2", + "default-features": false + }, + { + "name": "mp3lame", + "default-features": false + }, + { + "name": "portaudio", + "default-features": false + }, + { + "name": "sdl2", + "default-features": false, + "features": [ + "base" + ] + }, + { + "name": "zlib", + "default-features": false + } + ] +}