diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 197444968..ef3548170 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ concurrency: env: DISPLAY: ":99" # Display number to use for the X server GALLIUM_DRIVER: llvmpipe # Use Mesa 3D software OpenGL renderer + ANDROID_NDK_VERSION: "26.1.10909125" # Android NDK version to use defaults: run: @@ -35,13 +36,13 @@ jobs: - { name: Windows VS2022 Unity, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_UNITY_BUILD=ON -GNinja } - { name: Windows LLVM/Clang, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=clang++ -GNinja } - { name: Windows MinGW, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -GNinja } - - { name: Linux GCC, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=ON -GNinja } - - { name: Linux Clang, os: ubuntu-22.04, flags: -DCMAKE_CXX_COMPILER=clang++ -DSFML_RUN_DISPLAY_TESTS=ON -GNinja , gcovr_options: '--gcov-executable="llvm-cov-$CLANG_VERSION gcov"' } + - { name: Linux GCC, os: ubuntu-22.04, flags: -GNinja } + - { name: Linux Clang, os: ubuntu-22.04, flags: -DCMAKE_CXX_COMPILER=clang++ -GNinja , gcovr_options: '--gcov-executable="llvm-cov-$CLANG_VERSION gcov"' } - { name: Linux GCC DRM, os: ubuntu-22.04, flags: -DSFML_USE_DRM=ON -DSFML_RUN_DISPLAY_TESTS=OFF -GNinja } - { name: Linux GCC OpenGL ES, os: ubuntu-22.04, flags: -DSFML_OPENGL_ES=ON -DSFML_RUN_DISPLAY_TESTS=OFF -GNinja } - { name: macOS x64, os: macos-12, flags: -GNinja } - { name: macOS x64 Xcode, os: macos-12, flags: -GXcode } - - { name: macOS arm64, os: macos-14, flags: -GNinja } + - { name: macOS arm64, os: macos-14, flags: -GNinja -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF } - { name: iOS, os: macos-12, flags: -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 } - { name: iOS Xcode, os: macos-12, flags: -DCMAKE_SYSTEM_NAME=iOS -GXcode -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO } config: @@ -55,29 +56,56 @@ jobs: - platform: { name: Windows VS2022 x64, os: windows-2022 } config: { name: Static with PCH (MSVC), flags: -DSFML_USE_MESA3D=TRUE -GNinja -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } - platform: { name: Linux GCC, os: ubuntu-22.04 } - config: { name: Static with PCH (GCC), flags: -DSFML_RUN_DISPLAY_TESTS=ON -GNinja -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } + config: { name: Static with PCH (GCC), flags: -GNinja -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } - platform: { name: Linux Clang, os: ubuntu-22.04 } - config: { name: Static with PCH (Clang), flags: -DSFML_RUN_DISPLAY_TESTS=ON -GNinja -DCMAKE_CXX_COMPILER=clang++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } + config: { name: Static with PCH (Clang), flags: -GNinja -DCMAKE_CXX_COMPILER=clang++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } - platform: { name: Windows MinGW, os: windows-2022 } config: { name: Static Standard Libraries, flags: -GNinja -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -DSFML_USE_STATIC_STD_LIBS=TRUE } - platform: { name: Windows MinGW, os: windows-2022 } config: { name: Static with PCH (GCC), flags: -GNinja -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } - platform: { name: macOS, os: macos-12 } config: { name: Frameworks, flags: -GNinja -DSFML_BUILD_FRAMEWORKS=TRUE -DBUILD_SHARED_LIBS=TRUE } - - platform: { name: Android, os: ubuntu-22.04 } - config: { name: x86 (API 21), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=21 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: x86, api: 21 } - type: { name: Release } - - platform: { name: Android, os: ubuntu-22.04 } - config: { name: x86_64 (API 24), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86_64 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=24 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: x86_64, api: 24 } - type: { name: Release } - - platform: { name: Android, os: ubuntu-22.04 } - config: { name: armeabi-v7a (API 29), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=29 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: armeabi-v7a, api: 29 } - type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug } - - platform: { name: Android, os: ubuntu-22.04 } - config: { name: arm64-v8a (API 33), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=33 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: arm64-v8a, api: 33 } - type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug } - platform: { name: macOS , os: macos-12 } config: { name: System Deps, flags: -GNinja -DBUILD_SHARED_LIBS=TRUE -DSFML_USE_SYSTEM_DEPS=TRUE } + - platform: { name: Android, os: ubuntu-latest } + config: + name: x86 (API 21) + flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=21 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF + arch: x86 + api: 21 + libcxx: i686-linux-android/libc++_shared.so + emuarch: x86 + # emuapi: 29 # Removing this causes the tests to not run. This works around an issue that causes the test step to hang indefinitely. + type: { name: Release } + - platform: { name: Android, os: ubuntu-latest } + config: + name: x86_64 (API 24) + flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86_64 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=24 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF + arch: x86_64 + api: 24 + libcxx: x86_64-linux-android/libc++_shared.so + emuarch: x86_64 + # emuapi: 34 # Removing this causes the tests to not run. This works around an issue that causes the Network module tests to fail. + type: { name: Release } + - platform: { name: Android, os: ubuntu-latest } + config: + name: armeabi-v7a (API 29) + flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=29 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared + arch: armeabi-v7a + api: 29 + # There are no emulators available for armeabi-v7a so we skip running the tests (we still build them) by not specifying emuapi + type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug } + - platform: { name: Android, os: ubuntu-latest } + config: + name: arm64-v8a (API 33) + flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=33 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF + arch: arm64-v8a + api: 33 + libcxx: aarch64-linux-android/libc++_shared.so + emuarch: arm64-v8a + emuapi: 27 + emuflags: -qemu -machine virt + type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug } steps: @@ -107,11 +135,21 @@ jobs: echo "CLANG_VERSION=$CLANG_VERSION" >> $GITHUB_ENV sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox ccache gcovr ${{ matrix.platform.name == 'Linux Clang' && 'llvm-$CLANG_VERSION' || '' }} + - name: Remove ALSA Library + if: runner.os == 'Linux' && matrix.platform.name != 'Android' + run: sudo apt-get remove -y libasound2 + + # LIBCXX_SHARED_SO is the path used by CMake to copy the necessary runtime library to the AVD + # We find it by searching ANDROID_NDK_ROOT for file paths ending with matrix.config.libcxx - name: Install Android Components if: matrix.platform.name == 'Android' run: | echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;33.0.2" echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;26.1.10909125" + ANDROID_NDK_ROOT=$(echo $ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION) + echo "ANDROID_NDK_ROOT=$ANDROID_NDK_ROOT" >> $GITHUB_ENV + LIBCXX_SHARED_SO=$(find $ANDROID_NDK_ROOT -path \*/${{ matrix.config.libcxx }}) + echo "LIBCXX_SHARED_SO=$LIBCXX_SHARED_SO" >> $GITHUB_ENV - name: Install macOS Tools if: runner.os == 'macOS' @@ -197,11 +235,11 @@ jobs: # Make use of a test to print OpenGL vendor/renderer/version info to the console find build/bin -name test-sfml-window -or -name test-sfml-window.exe -exec sh -c "{} *sf::Context* --section=\"Version String\" --success | grep OpenGL" \; - - name: Test + - name: Test (Windows) if: runner.os == 'Windows' && !contains(matrix.platform.name, 'MinGW') run: cmake --build build --target runtests --config ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} - - name: Test + - name: Test (Linux/macOS/MinGW) if: (runner.os != 'Windows' || contains(matrix.platform.name, 'MinGW')) && !contains(matrix.platform.name, 'iOS') && !contains(matrix.platform.name, 'Android') run: | ctest --test-dir build --output-on-failure -C ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --repeat until-pass:3 @@ -210,6 +248,47 @@ jobs: gcovr -r $GITHUB_WORKSPACE -x build/coverage.out -s -f 'src/SFML/.*' -f 'include/SFML/.*' ${{ matrix.platform.gcovr_options }} $GITHUB_WORKSPACE fi + - name: Enable KVM + if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Cache AVD + if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.config.emuarch }}-${{ matrix.config.emuapi }} + + - name: Create AVD and Generate Snapshot for Caching + if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi && steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.config.emuapi }} + arch: ${{ matrix.config.emuarch }} + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none ${{ matrix.config.emuflags }} + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Test (Android) + if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.config.emuapi }} + arch: ${{ matrix.config.emuarch }} + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none ${{ matrix.config.emuflags }} + disable-animations: true + script: | + cmake --build build --config ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --target prepare-android-files + ctest --test-dir build --output-on-failure -C ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --repeat until-pass:3 + - name: Upload Coverage Report to Coveralls if: matrix.type.name == 'Debug' && github.repository == 'SFML/SFML' && !contains(matrix.platform.name, 'iOS') && !contains(matrix.platform.name, 'Android') # Disable upload in forks uses: coverallsapp/github-action@v2 @@ -311,7 +390,7 @@ jobs: fail-fast: false matrix: platform: - - { name: Linux, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=ON } + - { name: Linux, os: ubuntu-22.04, flags: } - { name: Linux DRM, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_USE_DRM=ON } - { name: Linux GCC OpenGL ES, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_OPENGL_ES=ON } @@ -327,7 +406,7 @@ jobs: - name: Install Linux Dependencies if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox + run: sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox && sudo apt-get remove -y libasound2 - name: Configure run: cmake --preset dev -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DSFML_BUILD_EXAMPLES=OFF -DSFML_ENABLE_SANITIZERS=ON ${{matrix.platform.flags}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 090d77d0f..84e87dd09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,9 +224,12 @@ endif() sfml_set_option(SFML_INSTALL_PKGCONFIG_FILES ${SFML_INSTALL_PKGCONFIG_DEFAULT} BOOL "TRUE to automatically install pkg-config files so other projects can find SFML") if(SFML_INSTALL_PKGCONFIG_FILES) + # account for CMAKE_INSTALL_LIBDIR potentially being an absolute path + file(RELATIVE_PATH SFML_RELATIVE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX} ${CMAKE_INSTALL_FULL_LIBDIR}) + # set pkgconfig install directory # this could be e.g. macports on mac or msys2 on windows etc. - set(SFML_PKGCONFIG_DIR "/${CMAKE_INSTALL_LIBDIR}/pkgconfig") + set(SFML_PKGCONFIG_DIR "/${SFML_RELATIVE_INSTALL_LIBDIR}/pkgconfig") if(SFML_OS_FREEBSD OR SFML_OS_OPENBSD OR SFML_OS_NETBSD) set(SFML_PKGCONFIG_DIR "/libdata/pkgconfig") @@ -465,6 +468,13 @@ endif() sfml_export_targets() +# configure extras by default when building SFML directly, otherwise hide them +sfml_set_option(SFML_CONFIGURE_EXTRAS ${PROJECT_IS_TOP_LEVEL} BOOL "TRUE to configure extras, FALSE to ignore them") + +if(NOT SFML_CONFIGURE_EXTRAS) + return() +endif() + set(CPACK_PACKAGE_NAME_SUMMARY "Simple and Fast Multimedia Library") set(CPACK_PACKAGE_VENDOR "SFML Team") set(CPACK_PACKAGE_FILE_NAME "SFML-${PROJECT_VERSION}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}") @@ -485,13 +495,6 @@ set(CPACK_NSIS_INSTALLER_MUI_ICON_CODE "!define MUI_WELCOMEFINISHPAGE_BITMAP \\\ include(CPack) -# configure extras by default when building SFML directly, otherwise hide them -sfml_set_option(SFML_CONFIGURE_EXTRAS ${PROJECT_IS_TOP_LEVEL} BOOL "TRUE to configure extras, FALSE to ignore them") - -if(NOT SFML_CONFIGURE_EXTRAS) - return() -endif() - # add an option for building the API documentation sfml_set_option(SFML_BUILD_DOC FALSE BOOL "TRUE to generate the API documentation, FALSE to ignore it") if(SFML_BUILD_DOC) diff --git a/changelog.md b/changelog.md index db2f95beb..365e5aecd 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,8 @@ - Ensure GNUInstallDirs cache vars are included before first used (#2778, #2779) - [macOS] Fix incorrect variable expansion (#2780) +- Issue warning when trying to use UCRT MinGW with precompiled MSVCRT depenencies (#2821) +- Fix Nix pkg-config support ### Audio diff --git a/cmake/Macros.cmake b/cmake/Macros.cmake index 38e97098a..919b6d4f4 100644 --- a/cmake/Macros.cmake +++ b/cmake/Macros.cmake @@ -368,6 +368,11 @@ function(sfml_add_test target SOURCES DEPENDS) # Delay test registration when cross compiling to avoid running crosscompiled app on host OS if(CMAKE_CROSSCOMPILING) set(CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + + # When running tests on Android, use a custom shell script to invoke commands using adb shell + if(SFML_OS_ANDROID) + set_target_properties(${target} PROPERTIES CROSSCOMPILING_EMULATOR "${PROJECT_BINARY_DIR}/run-in-adb-shell.sh") + endif() endif() # Add the test diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 0cd0543a4..52bd326db 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -26,7 +26,12 @@ if(SFML_OS_WINDOWS) PATHS "C:/Program Files/HTML Help Workshop" "C:/Program Files (x86)/HTML Help Workshop" DOC "HTML Help Compiler program") if(DOXYGEN_HHC_PROGRAM) - set(DOXYGEN_GENERATE_HTMLHELP YES) + if(DOXYGEN_VERSION VERSION_LESS "1.10.0") + set(DOXYGEN_GENERATE_HTMLHELP YES) + else() + message("Due to conflicts with the HTML output settings in Doxygen ${DOXYGEN_VERSION}, the HTML Help generation will be disabled") + set(DOXYGEN_GENERATE_HTMLHELP NO) + endif() else() set(DOXYGEN_GENERATE_HTMLHELP NO) endif() diff --git a/doc/doxyfile.in b/doc/doxyfile.in index 900d46f90..beb5d1484 100644 --- a/doc/doxyfile.in +++ b/doc/doxyfile.in @@ -1,4 +1,4 @@ -# Doxyfile 1.9.6 +# Doxyfile 1.10.0 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -63,6 +63,12 @@ PROJECT_BRIEF = PROJECT_LOGO = +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is # entered, it will be relative to the location where doxygen was started. If @@ -380,6 +386,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 0 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -504,6 +521,14 @@ LOOKUP_CACHE_SIZE = 0 NUM_PROC_THREADS = 1 +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -516,7 +541,7 @@ NUM_PROC_THREADS = 1 # normally produced when WARNINGS is set to YES. # The default value is: NO. -EXTRACT_ALL = NO +EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # be included in the documentation. @@ -889,7 +914,14 @@ WARN_IF_UNDOC_ENUM_VAL = NO # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # at the end of the doxygen process doxygen will return with a non-zero status. -# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -968,12 +1000,12 @@ INPUT_FILE_ENCODING = # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, -# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C -# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.hpp @@ -1020,9 +1052,6 @@ EXCLUDE_PATTERNS = .git \ # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = priv @@ -1136,7 +1165,8 @@ FORTRAN_COMMENT_AFTER = 72 SOURCE_BROWSER = YES # Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. +# multi-line macros, enums or list initialized variables directly into the +# documentation. # The default value is: NO. INLINE_SOURCES = NO @@ -1384,7 +1414,7 @@ HTML_COLORSTYLE = LIGHT # Minimum value: 0, maximum value: 359, default value: 220. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_HUE = 85 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors # in the HTML output. For a value of 0 the output will use gray-scales only. A @@ -1405,15 +1435,6 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1433,6 +1454,33 @@ HTML_DYNAMIC_MENUS = NO HTML_DYNAMIC_SECTIONS = NO +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1563,6 +1611,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -2051,9 +2109,16 @@ PDF_HYPERLINKS = YES USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2074,14 +2139,6 @@ LATEX_HIDE_INDICES = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -2247,7 +2304,7 @@ DOCBOOK_OUTPUT = docbook #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. @@ -2258,6 +2315,28 @@ GENERATE_AUTOGEN_DEF = NO # Configuration options related to Sqlite3 output #--------------------------------------------------------------------------- +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2405,15 +2484,15 @@ TAGFILES = GENERATE_TAGFILE = "@DOXYGEN_OUTPUT_DIR@/SFML.tag" -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2427,16 +2506,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2445,7 +2517,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: NO. @@ -2498,13 +2570,19 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a -# graph for each documented class showing the direct and indirect inheritance -# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, -# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set -# to TEXT the direct and indirect inheritance relations will be shown as texts / -# links. -# Possible values are: NO, YES, TEXT and GRAPH. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. Explicit enabling an inheritance +# graph or choosing a different representation for an inheritance graph of a +# specific class, can be accomplished by means of the command \inheritancegraph. +# Disabling an inheritance graph can be accomplished by means of the command +# \hideinheritancegraph. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. CLASS_GRAPH = YES @@ -2512,15 +2590,21 @@ CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. See also the chapter Grouping -# in the manual. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2562,8 +2646,8 @@ DOT_UML_DETAILS = NO # The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters # to display on a single line. If the actual line length exceeds this threshold -# significantly it will wrapped across multiple lines. Some heuristics are apply -# to avoid ugly line breaks. +# significantly it will be wrapped across multiple lines. Some heuristics are +# applied to avoid ugly line breaks. # Minimum value: 0, maximum value: 1000, default value: 17. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2580,7 +2664,9 @@ TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2589,7 +2675,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2629,7 +2718,10 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2645,7 +2737,7 @@ DIR_GRAPH_MAX_DEPTH = 1 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). @@ -2682,11 +2774,12 @@ DOT_PATH = DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2763,3 +2856,19 @@ GENERATE_LEGEND = YES # The default value is: YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/doc/doxygen.css b/doc/doxygen.css index 632fbbaab..a7a144818 100644 --- a/doc/doxygen.css +++ b/doc/doxygen.css @@ -147,12 +147,53 @@ div.fragment { font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 10pt; + position: relative; padding: 0.5em 1em; background-color: #f5f5f5; border: 1px solid #bbb; border-radius(5px); } +.clipboard { + width: 24px; + height: 24px; + right: 5px; + top: 5px; + opacity: 0; + position: absolute; + display: inline; + overflow: auto; + fill: black; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.clipboard.success { + border: 1px solid black; + border-radius: 4px; +} + +.fragment:hover .clipboard, .clipboard.success { + opacity: .28; +} + +.clipboard:hover, .clipboard.success { + opacity: 1 !important; +} + +.clipboard:active:not([class~=success]) svg { + transform: scale(.91); +} + +.clipboard.success svg { + fill: #2EC82E; +} + +.clipboard.success { + border-color: #2EC82E; +} + div.line { min-height: 13px; text-wrap: unrestricted; @@ -1254,7 +1295,7 @@ div.contents ul li { width: 24px; height: 18px; margin-bottom: 4px; - background-image:url('folderopen.png'); + background-image:url('folderopen.svg'); background-position: 0px -4px; background-repeat: repeat-y; vertical-align:top; @@ -1265,7 +1306,7 @@ div.contents ul li { width: 24px; height: 18px; margin-bottom: 4px; - background-image:url('folderclosed.png'); + background-image:url('folderclosed.svg'); background-position: 0px -4px; background-repeat: repeat-y; vertical-align:top; @@ -1276,7 +1317,7 @@ div.contents ul li { width: 24px; height: 18px; margin-bottom: 4px; - background-image:url('doc.png'); + background-image:url('doc.svg'); background-position: 0px -4px; background-repeat: repeat-y; vertical-align:top; diff --git a/doc/footer.html b/doc/footer.html index c9cd93754..c42870da2 100644 --- a/doc/footer.html +++ b/doc/footer.html @@ -1,7 +1,7 @@
+
diff --git a/examples/sound_effects/SoundEffects.cpp b/examples/sound_effects/SoundEffects.cpp index 77404e4c0..704a0cb2a 100644 --- a/examples/sound_effects/SoundEffects.cpp +++ b/examples/sound_effects/SoundEffects.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,7 @@ namespace constexpr auto windowWidth = 800u; constexpr auto windowHeight = 600u; constexpr auto pi = 3.14159265359f; +constexpr auto sqrt2 = 2.0f * 0.707106781186547524401f; std::filesystem::path resourcesDir() { @@ -84,10 +86,10 @@ protected: private: // Virtual functions to be implemented in derived effects - virtual void onUpdate(float time, float x, float y) = 0; - virtual void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const = 0; - virtual void onStart() = 0; - virtual void onStop() = 0; + virtual void onUpdate(float time, float x, float y) = 0; + virtual void onDraw(sf::RenderTarget& target, sf::RenderStates states) const = 0; + virtual void onStart() = 0; + virtual void onStop() = 0; virtual void onKey(sf::Keyboard::Key) { @@ -113,7 +115,7 @@ public: // Load the music file if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) - sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; + std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; // Set the music to loop m_music.setLoop(true); @@ -128,7 +130,7 @@ public: m_music.setPosition({m_position.x, m_position.y, 0.f}); } - void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override + void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override { auto statesCopy(states); statesCopy.transform = sf::Transform::Identity; @@ -172,7 +174,7 @@ public: { // Load the music file if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) - sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; + std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; // Set the music to loop m_music.setLoop(true); @@ -202,7 +204,7 @@ public: m_volumeText.setString("Volume: " + std::to_string(m_volume)); } - void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override + void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override { target.draw(m_pitchText, states); target.draw(m_volumeText, states); @@ -275,7 +277,7 @@ public: // Load the music file if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) - sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; + std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; // Set the music to loop m_music.setLoop(true); @@ -305,7 +307,7 @@ public: m_music.setPosition({m_position.x, m_position.y, 0.f}); } - void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override + void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override { auto statesCopy(states); @@ -375,7 +377,7 @@ public: m_currentFrequency.setString("Frequency: " + std::to_string(m_frequency) + " Hz"); } - void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override + void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override { target.draw(m_instruction, states); target.draw(m_currentType, states); @@ -547,7 +549,7 @@ public: setDopplerFactor(m_factor); } - void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override + void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override { auto statesCopy(states); statesCopy.transform = sf::Transform::Identity; @@ -615,6 +617,425 @@ private: }; +//////////////////////////////////////////////////////////// +// Processing base class +//////////////////////////////////////////////////////////// +class Processing : public Effect +{ +public: + void onUpdate([[maybe_unused]] float time, float x, float y) override + { + m_position = {windowWidth * x - 10.f, windowHeight * y - 10.f}; + m_music.setPosition({m_position.x, m_position.y, 0.f}); + } + + void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override + { + auto statesCopy(states); + statesCopy.transform = sf::Transform::Identity; + statesCopy.transform.translate(m_position); + + target.draw(m_listener, states); + target.draw(m_soundShape, statesCopy); + target.draw(m_enabledText); + target.draw(m_instructions); + } + + void onStart() override + { + // Synchronize listener audio position with graphical position + sf::Listener::setPosition({m_listener.getPosition().x, m_listener.getPosition().y, 0.f}); + + m_music.play(); + } + + void onStop() override + { + m_music.stop(); + } + +protected: + Processing(std::string name) : + Effect(std::move(name)), + m_enabledText(getFont(), "Processing: Enabled"), + m_instructions(getFont(), "Press Space to enable/disable processing") + { + m_listener.setPosition({(windowWidth - 20.f) / 2.f, (windowHeight - 20.f) / 2.f}); + m_listener.setFillColor(sf::Color::Red); + + m_enabledText.setPosition({windowWidth / 2.f - 120.f, windowHeight * 3.f / 4.f - 50.f}); + m_instructions.setPosition({windowWidth / 2.f - 250.f, windowHeight * 3.f / 4.f}); + + // Load the music file + if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) + std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; + + // Set the music to loop + m_music.setLoop(true); + + // Set attenuation to a nice value + m_music.setAttenuation(0.0f); + } + + sf::Music& getMusic() + { + return m_music; + } + + const std::shared_ptr& getEnabled() const + { + return m_enabled; + } + +private: + void onKey(sf::Keyboard::Key key) override + { + if (key == sf::Keyboard::Key::Space) + *m_enabled = !*m_enabled; + + m_enabledText.setString(*m_enabled ? "Processing: Enabled" : "Processing: Disabled"); + } + + sf::CircleShape m_listener{20.f}; + sf::CircleShape m_soundShape{20.f}; + sf::Vector2f m_position; + sf::Music m_music; + std::shared_ptr m_enabled{std::make_shared(true)}; + sf::Text m_enabledText; + sf::Text m_instructions; +}; + + +//////////////////////////////////////////////////////////// +// Biquad Filter (https://github.com/dimtass/DSP-Cpp-filters) +//////////////////////////////////////////////////////////// +class BiquadFilter : public Processing +{ +protected: + struct Coefficients + { + float a0{}; + float a1{}; + float a2{}; + float b1{}; + float b2{}; + float c0{}; + float d0{}; + }; + + using Processing::Processing; + + void setCoefficients(const Coefficients& coefficients) + { + auto& music = getMusic(); + + struct State + { + float xnz1{}; + float xnz2{}; + float ynz1{}; + float ynz2{}; + }; + + // We use a mutable lambda to tie the lifetime of the state and coefficients to the lambda itself + // This is necessary since the Echo object will be destroyed before the Music object + // While the Music object exists, it is possible that the audio engine will try to call + // this lambda hence we need to always have usable coefficients and state until the Music and the + // associated lambda are destroyed + music.setEffectProcessor( + [coefficients, + enabled = getEnabled(), + state = std::vector(music.getChannelCount())](const float* inputFrames, + unsigned int& inputFrameCount, + float* outputFrames, + unsigned int& outputFrameCount, + unsigned int frameChannelCount) mutable + { + for (auto frame = 0u; frame < outputFrameCount; ++frame) + { + for (auto channel = 0u; channel < frameChannelCount; ++channel) + { + auto& channelState = state[channel]; + + const auto xn = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available + const auto yn = coefficients.a0 * xn + coefficients.a1 * channelState.xnz1 + + coefficients.a2 * channelState.xnz2 - coefficients.b1 * channelState.ynz1 - + coefficients.b2 * channelState.ynz2; + + channelState.xnz2 = channelState.xnz1; + channelState.xnz1 = xn; + channelState.ynz2 = channelState.ynz1; + channelState.ynz1 = yn; + + outputFrames[channel] = *enabled ? yn : xn; + } + + inputFrames += (inputFrames ? frameChannelCount : 0u); + outputFrames += frameChannelCount; + } + + // We processed data 1:1 + inputFrameCount = outputFrameCount; + }); + } +}; + + +//////////////////////////////////////////////////////////// +// High-pass Filter (https://github.com/dimtass/DSP-Cpp-filters) +//////////////////////////////////////////////////////////// +struct HighPassFilter : BiquadFilter +{ + HighPassFilter() : BiquadFilter("High-pass Filter") + { + static constexpr auto cutoffFrequency = 2000.f; + + const auto c = std::tan(pi * cutoffFrequency / static_cast(getMusic().getSampleRate())); + + Coefficients coefficients; + + coefficients.a0 = 1.f / (1.f + sqrt2 * c + std::pow(c, 2.f)); + coefficients.a1 = -2.f * coefficients.a0; + coefficients.a2 = coefficients.a0; + coefficients.b1 = 2.f * coefficients.a0 * (std::pow(c, 2.f) - 1.f); + coefficients.b2 = coefficients.a0 * (1.f - sqrt2 * c + std::pow(c, 2.f)); + + setCoefficients(coefficients); + } +}; + + +//////////////////////////////////////////////////////////// +// Low-pass Filter (https://github.com/dimtass/DSP-Cpp-filters) +//////////////////////////////////////////////////////////// +struct LowPassFilter : BiquadFilter +{ + LowPassFilter() : BiquadFilter("Low-pass Filter") + { + static constexpr auto cutoffFrequency = 500.f; + + const auto c = 1.f / std::tan(pi * cutoffFrequency / static_cast(getMusic().getSampleRate())); + + Coefficients coefficients; + + coefficients.a0 = 1.f / (1.f + sqrt2 * c + std::pow(c, 2.f)); + coefficients.a1 = 2.f * coefficients.a0; + coefficients.a2 = coefficients.a0; + coefficients.b1 = 2.f * coefficients.a0 * (1.f - std::pow(c, 2.f)); + coefficients.b2 = coefficients.a0 * (1.f - sqrt2 * c + std::pow(c, 2.f)); + + setCoefficients(coefficients); + } +}; + + +//////////////////////////////////////////////////////////// +// Echo (miniaudio implementation) +//////////////////////////////////////////////////////////// +struct Echo : Processing +{ + Echo() : Processing("Echo") + { + auto& music = getMusic(); + + static constexpr auto delay = 0.2f; + static constexpr auto decay = 0.75f; + static constexpr auto wet = 0.8f; + static constexpr auto dry = 1.f; + + const auto channelCount = music.getChannelCount(); + const auto sampleRate = music.getSampleRate(); + const auto delayInFrames = static_cast(static_cast(sampleRate) * delay); + + // We use a mutable lambda to tie the lifetime of the state to the lambda itself + // This is necessary since the Echo object will be destroyed before the Music object + // While the Music object exists, it is possible that the audio engine will try to call + // this lambda hence we need to always have a usable state until the Music and the + // associated lambda are destroyed + music.setEffectProcessor( + [delayInFrames, + enabled = getEnabled(), + buffer = std::vector(delayInFrames * channelCount, 0.f), + cursor = 0u](const float* inputFrames, + unsigned int& inputFrameCount, + float* outputFrames, + unsigned int& outputFrameCount, + unsigned int frameChannelCount) mutable + { + for (auto frame = 0u; frame < outputFrameCount; ++frame) + { + for (auto channel = 0u; channel < frameChannelCount; ++channel) + { + const auto input = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available + const auto bufferIndex = (cursor * frameChannelCount) + channel; + buffer[bufferIndex] = (buffer[bufferIndex] * decay) + (input * dry); + outputFrames[channel] = *enabled ? buffer[bufferIndex] * wet : input; + } + + cursor = (cursor + 1) % delayInFrames; + + inputFrames += (inputFrames ? frameChannelCount : 0u); + outputFrames += frameChannelCount; + } + + // We processed data 1:1 + inputFrameCount = outputFrameCount; + }); + } +}; + + +//////////////////////////////////////////////////////////// +// Reverb (https://github.com/sellicott/DSP-FFMpeg-Reverb) +//////////////////////////////////////////////////////////// +class Reverb : public Processing +{ +public: + Reverb() : Processing("Reverb") + { + auto& music = getMusic(); + + static constexpr auto sustain = 0.7f; // [0.f; 1.f] + + const auto channelCount = music.getChannelCount(); + const auto sampleRate = music.getSampleRate(); + + std::vector> filters; + filters.reserve(channelCount); + + for (auto i = 0u; i < channelCount; ++i) + filters.emplace_back(sampleRate, sustain); + + // We use a mutable lambda to tie the lifetime of the state to the lambda itself + // This is necessary since the Echo object will be destroyed before the Music object + // While the Music object exists, it is possible that the audio engine will try to call + // this lambda hence we need to always have a usable state until the Music and the + // associated lambda are destroyed + music.setEffectProcessor( + [filters, enabled = getEnabled()](const float* inputFrames, + unsigned int& inputFrameCount, + float* outputFrames, + unsigned int& outputFrameCount, + unsigned int frameChannelCount) mutable + { + for (auto frame = 0u; frame < outputFrameCount; ++frame) + { + for (auto channel = 0u; channel < frameChannelCount; ++channel) + { + const auto input = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available + outputFrames[channel] = *enabled ? filters[channel](input) : input; + } + + inputFrames += (inputFrames ? frameChannelCount : 0u); + outputFrames += frameChannelCount; + } + + // We processed data 1:1 + inputFrameCount = outputFrameCount; + }); + } + +private: + template + class AllPassFilter + { + public: + AllPassFilter(std::size_t delay, float theGain) : m_buffer(delay, {}), m_gain(theGain) + { + } + + T operator()(T input) + { + const auto output = m_buffer[m_cursor]; + input = static_cast(input + m_gain * output); + m_buffer[m_cursor] = input; + m_cursor = (m_cursor + 1) % m_buffer.size(); + return static_cast(-m_gain * input + output); + } + + private: + std::vector m_buffer; + std::size_t m_cursor{}; + const float m_gain{}; + }; + + + template + class FIRFilter + { + public: + FIRFilter(std::vector taps) : m_taps(std::move(taps)) + { + } + + T operator()(T input) + { + m_buffer[m_cursor] = input; + m_cursor = (m_cursor + 1) % m_buffer.size(); + + T output{}; + + for (auto i = 0u; i < m_taps.size(); ++i) + output += static_cast(m_taps[i] * m_buffer[(m_cursor + i) % m_buffer.size()]); + + return output; + } + + private: + const std::vector m_taps; + std::vector m_buffer = std::vector(m_taps.size(), {}); + std::size_t m_cursor{}; + }; + + template + class ReverbFilter + { + public: + ReverbFilter(unsigned int sampleRate, float feedbackGain) : + m_allPass{{sampleRate / 10, 0.6f}, {sampleRate / 30, -0.6f}, {sampleRate / 90, 0.6f}, {sampleRate / 270, -0.6f}}, + m_fir({0.003369f, 0.002810f, 0.001758f, 0.000340f, -0.001255f, -0.002793f, -0.004014f, -0.004659f, + -0.004516f, -0.003464f, -0.001514f, 0.001148f, 0.004157f, 0.006986f, 0.009003f, 0.009571f, + 0.008173f, 0.004560f, -0.001120f, -0.008222f, -0.015581f, -0.021579f, -0.024323f, -0.021933f, + -0.012904f, 0.003500f, 0.026890f, 0.055537f, 0.086377f, 0.115331f, 0.137960f, 0.150407f, + 0.150407f, 0.137960f, 0.115331f, 0.086377f, 0.055537f, 0.026890f, 0.003500f, -0.012904f, + -0.021933f, -0.024323f, -0.021579f, -0.015581f, -0.008222f, -0.001120f, 0.004560f, 0.008173f, + 0.009571f, 0.009003f, 0.006986f, 0.004157f, 0.001148f, -0.001514f, -0.003464f, -0.004516f, + -0.004659f, -0.004014f, -0.002793f, -0.001255f, 0.000340f, 0.001758f, 0.002810f, 0.003369f}), + m_buffer(sampleRate / 5, {}), // sample rate / 5 = 200ms buffer size + m_feedbackGain(feedbackGain) + { + } + + T operator()(T input) + { + auto output = static_cast(0.7f * input + m_feedbackGain * m_buffer[m_cursor]); + + for (auto& f : m_allPass) + output = f(output); + + output = m_fir(output); + + m_buffer[m_cursor] = output; + m_cursor = (m_cursor + 1) % m_buffer.size(); + + output += 0.5f * m_buffer[(m_cursor + 1 * m_interval - 1) % m_buffer.size()]; + output += 0.25f * m_buffer[(m_cursor + 2 * m_interval - 1) % m_buffer.size()]; + output += 0.125f * m_buffer[(m_cursor + 3 * m_interval - 1) % m_buffer.size()]; + + return 0.6f * output + input; + } + + private: + AllPassFilter m_allPass[4]; + FIRFilter m_fir; + std::vector m_buffer; + std::size_t m_cursor{}; + const std::size_t m_interval{m_buffer.size() / 3}; + const float m_feedbackGain{}; + }; +}; + + //////////////////////////////////////////////////////////// /// Entry point of application /// @@ -636,19 +1057,25 @@ int main() Effect::setFont(font); // Create the effects - Surround surroundEffect; - PitchVolume pitchVolumeEffect; - Attenuation attenuationEffect; - Tone toneEffect; - Doppler dopplerEffect; + Surround surroundEffect; + PitchVolume pitchVolumeEffect; + Attenuation attenuationEffect; + Tone toneEffect; + Doppler dopplerEffect; + HighPassFilter highPassFilterEffect; + LowPassFilter lowPassFilterEffect; + Echo echoEffect; + Reverb reverbEffect; - const std::array effects{ - &surroundEffect, - &pitchVolumeEffect, - &attenuationEffect, - &toneEffect, - &dopplerEffect, - }; + const std::array effects{&surroundEffect, + &pitchVolumeEffect, + &attenuationEffect, + &toneEffect, + &dopplerEffect, + &highPassFilterEffect, + &lowPassFilterEffect, + &echoEffect, + &reverbEffect}; std::size_t current = 0; diff --git a/extlibs/headers/stb_image/stb_image.h b/extlibs/headers/stb_image/stb_image.h index 5e807a0a6..a632d5435 100644 --- a/extlibs/headers/stb_image/stb_image.h +++ b/extlibs/headers/stb_image/stb_image.h @@ -1,4 +1,4 @@ -/* stb_image - v2.28 - public domain image loader - http://nothings.org/stb +/* stb_image - v2.29 - public domain image loader - http://nothings.org/stb no warranty implied; use at your own risk Do this: @@ -48,6 +48,7 @@ LICENSE RECENT REVISION HISTORY: + 2.29 (2023-05-xx) optimizations 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes 2.26 (2020-07-13) many minor fixes @@ -1072,8 +1073,8 @@ static int stbi__addints_valid(int a, int b) return a <= INT_MAX - b; } -// returns 1 if the product of two signed shorts is valid, 0 on overflow. -static int stbi__mul2shorts_valid(short a, short b) +// returns 1 if the product of two ints fits in a signed short, 0 on overflow. +static int stbi__mul2shorts_valid(int a, int b) { if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid @@ -3384,13 +3385,13 @@ static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) return 1; } -static int stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) +static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) { // some JPEGs have junk at end, skip over it but if we find what looks // like a valid marker, resume there while (!stbi__at_eof(j->s)) { - int x = stbi__get8(j->s); - while (x == 255) { // might be a marker + stbi_uc x = stbi__get8(j->s); + while (x == 0xff) { // might be a marker if (stbi__at_eof(j->s)) return STBI__MARKER_none; x = stbi__get8(j->s); if (x != 0x00 && x != 0xff) { @@ -4176,6 +4177,7 @@ typedef struct { stbi_uc *zbuffer, *zbuffer_end; int num_bits; + int hit_zeof_once; stbi__uint32 code_buffer; char *zout; @@ -4242,9 +4244,20 @@ stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) int b,s; if (a->num_bits < 16) { if (stbi__zeof(a)) { - return -1; /* report error for unexpected end of data. */ + if (!a->hit_zeof_once) { + // This is the first time we hit eof, insert 16 extra padding btis + // to allow us to keep going; if we actually consume any of them + // though, that is invalid data. This is caught later. + a->hit_zeof_once = 1; + a->num_bits += 16; // add 16 implicit zero bits + } else { + // We already inserted our extra 16 padding bits and are again + // out, this stream is actually prematurely terminated. + return -1; + } + } else { + stbi__fill_bits(a); } - stbi__fill_bits(a); } b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; if (b) { @@ -4309,6 +4322,13 @@ static int stbi__parse_huffman_block(stbi__zbuf *a) int len,dist; if (z == 256) { a->zout = zout; + if (a->hit_zeof_once && a->num_bits < 16) { + // The first time we hit zeof, we inserted 16 extra zero bits into our bit + // buffer so the decoder can just do its speculative decoding. But if we + // actually consumed any of those bits (which is the case when num_bits < 16), + // the stream actually read past the end so it is malformed. + return stbi__err("unexpected end","Corrupt PNG"); + } return 1; } if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data @@ -4320,7 +4340,7 @@ static int stbi__parse_huffman_block(stbi__zbuf *a) dist = stbi__zdist_base[z]; if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); - if (zout + len > a->zout_end) { + if (len > a->zout_end - zout) { if (!stbi__zexpand(a, zout, len)) return 0; zout = a->zout; } @@ -4464,6 +4484,7 @@ static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) if (!stbi__parse_zlib_header(a)) return 0; a->num_bits = 0; a->code_buffer = 0; + a->hit_zeof_once = 0; do { final = stbi__zreceive(a,1); type = stbi__zreceive(a,2); @@ -4619,9 +4640,8 @@ enum { STBI__F_up=2, STBI__F_avg=3, STBI__F_paeth=4, - // synthetic filters used for first scanline to avoid needing a dummy row of 0s - STBI__F_avg_first, - STBI__F_paeth_first + // synthetic filter used for first scanline to avoid needing a dummy row of 0s + STBI__F_avg_first }; static stbi_uc first_row_filter[5] = @@ -4630,29 +4650,56 @@ static stbi_uc first_row_filter[5] = STBI__F_sub, STBI__F_none, STBI__F_avg_first, - STBI__F_paeth_first + STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub }; static int stbi__paeth(int a, int b, int c) { - int p = a + b - c; - int pa = abs(p-a); - int pb = abs(p-b); - int pc = abs(p-c); - if (pa <= pb && pa <= pc) return a; - if (pb <= pc) return b; - return c; + // This formulation looks very different from the reference in the PNG spec, but is + // actually equivalent and has favorable data dependencies and admits straightforward + // generation of branch-free code, which helps performance significantly. + int thresh = c*3 - (a + b); + int lo = a < b ? a : b; + int hi = a < b ? b : a; + int t0 = (hi <= thresh) ? lo : c; + int t1 = (thresh <= lo) ? hi : t0; + return t1; } static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; +// adds an extra all-255 alpha channel +// dest == src is legal +// img_n must be 1 or 3 +static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) +{ + int i; + // must process data backwards since we allow dest==src + if (img_n == 1) { + for (i=x-1; i >= 0; --i) { + dest[i*2+1] = 255; + dest[i*2+0] = src[i]; + } + } else { + STBI_ASSERT(img_n == 3); + for (i=x-1; i >= 0; --i) { + dest[i*4+3] = 255; + dest[i*4+2] = src[i*3+2]; + dest[i*4+1] = src[i*3+1]; + dest[i*4+0] = src[i*3+0]; + } + } +} + // create the png data from post-deflated data static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) { - int bytes = (depth == 16? 2 : 1); + int bytes = (depth == 16 ? 2 : 1); stbi__context *s = a->s; stbi__uint32 i,j,stride = x*out_n*bytes; stbi__uint32 img_len, img_width_bytes; + stbi_uc *filter_buf; + int all_ok = 1; int k; int img_n = s->img_n; // copy it into a local for later @@ -4664,8 +4711,11 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into if (!a->out) return stbi__err("outofmem", "Out of memory"); + // note: error exits here don't need to clean up a->out individually, + // stbi__do_png always does on error. if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); img_width_bytes = (((img_n * x * depth) + 7) >> 3); + if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); img_len = (img_width_bytes + 1) * y; // we used to check for exact match between raw_len and img_len on non-interlaced PNGs, @@ -4673,189 +4723,137 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r // so just check for raw_len < img_len always. if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); + // Allocate two scan lines worth of filter workspace buffer. + filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); + if (!filter_buf) return stbi__err("outofmem", "Out of memory"); + + // Filtering for low-bit-depth images + if (depth < 8) { + filter_bytes = 1; + width = img_width_bytes; + } + for (j=0; j < y; ++j) { - stbi_uc *cur = a->out + stride*j; - stbi_uc *prior; + // cur/prior filter buffers alternate + stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; + stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; + stbi_uc *dest = a->out + stride*j; + int nk = width * filter_bytes; int filter = *raw++; - if (filter > 4) - return stbi__err("invalid filter","Corrupt PNG"); - - if (depth < 8) { - if (img_width_bytes > x) return stbi__err("invalid width","Corrupt PNG"); - cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place - filter_bytes = 1; - width = img_width_bytes; + // check filter type + if (filter > 4) { + all_ok = stbi__err("invalid filter","Corrupt PNG"); + break; } - prior = cur - stride; // bugfix: need to compute this after 'cur +=' computation above // if first row, use special filter that doesn't sample previous row if (j == 0) filter = first_row_filter[filter]; - // handle first byte explicitly - for (k=0; k < filter_bytes; ++k) { - switch (filter) { - case STBI__F_none : cur[k] = raw[k]; break; - case STBI__F_sub : cur[k] = raw[k]; break; - case STBI__F_up : cur[k] = STBI__BYTECAST(raw[k] + prior[k]); break; - case STBI__F_avg : cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); break; - case STBI__F_paeth : cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(0,prior[k],0)); break; - case STBI__F_avg_first : cur[k] = raw[k]; break; - case STBI__F_paeth_first: cur[k] = raw[k]; break; - } + // perform actual filtering + switch (filter) { + case STBI__F_none: + memcpy(cur, raw, nk); + break; + case STBI__F_sub: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); + break; + case STBI__F_up: + for (k = 0; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + break; + case STBI__F_avg: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); + break; + case STBI__F_paeth: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); + break; + case STBI__F_avg_first: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); + break; } - if (depth == 8) { - if (img_n != out_n) - cur[img_n] = 255; // first pixel - raw += img_n; - cur += out_n; - prior += out_n; - } else if (depth == 16) { - if (img_n != out_n) { - cur[filter_bytes] = 255; // first pixel top byte - cur[filter_bytes+1] = 255; // first pixel bottom byte - } - raw += filter_bytes; - cur += output_bytes; - prior += output_bytes; - } else { - raw += 1; - cur += 1; - prior += 1; - } + raw += nk; - // this is a little gross, so that we don't switch per-pixel or per-component - if (depth < 8 || img_n == out_n) { - int nk = (width - 1)*filter_bytes; - #define STBI__CASE(f) \ - case f: \ - for (k=0; k < nk; ++k) - switch (filter) { - // "none" filter turns into a memcpy here; make that explicit. - case STBI__F_none: memcpy(cur, raw, nk); break; - STBI__CASE(STBI__F_sub) { cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); } break; - STBI__CASE(STBI__F_up) { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break; - STBI__CASE(STBI__F_avg) { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); } break; - STBI__CASE(STBI__F_paeth) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],prior[k],prior[k-filter_bytes])); } break; - STBI__CASE(STBI__F_avg_first) { cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); } break; - STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],0,0)); } break; - } - #undef STBI__CASE - raw += nk; - } else { - STBI_ASSERT(img_n+1 == out_n); - #define STBI__CASE(f) \ - case f: \ - for (i=x-1; i >= 1; --i, cur[filter_bytes]=255,raw+=filter_bytes,cur+=output_bytes,prior+=output_bytes) \ - for (k=0; k < filter_bytes; ++k) - switch (filter) { - STBI__CASE(STBI__F_none) { cur[k] = raw[k]; } break; - STBI__CASE(STBI__F_sub) { cur[k] = STBI__BYTECAST(raw[k] + cur[k- output_bytes]); } break; - STBI__CASE(STBI__F_up) { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break; - STBI__CASE(STBI__F_avg) { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k- output_bytes])>>1)); } break; - STBI__CASE(STBI__F_paeth) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],prior[k],prior[k- output_bytes])); } break; - STBI__CASE(STBI__F_avg_first) { cur[k] = STBI__BYTECAST(raw[k] + (cur[k- output_bytes] >> 1)); } break; - STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],0,0)); } break; - } - #undef STBI__CASE - - // the loop above sets the high byte of the pixels' alpha, but for - // 16 bit png files we also need the low byte set. we'll do that here. - if (depth == 16) { - cur = a->out + stride*j; // start at the beginning of the row again - for (i=0; i < x; ++i,cur+=output_bytes) { - cur[filter_bytes+1] = 255; - } - } - } - } - - // we make a separate pass to expand bits to pixels; for performance, - // this could run two scanlines behind the above code, so it won't - // intefere with filtering but will still be in the cache. - if (depth < 8) { - for (j=0; j < y; ++j) { - stbi_uc *cur = a->out + stride*j; - stbi_uc *in = a->out + stride*j + x*out_n - img_width_bytes; - // unpack 1/2/4-bit into a 8-bit buffer. allows us to keep the common 8-bit path optimal at minimal cost for 1/2/4-bit - // png guarante byte alignment, if width is not multiple of 8/4/2 we'll decode dummy trailing data that will be skipped in the later loop + // expand decoded bits in cur to dest, also adding an extra alpha channel if desired + if (depth < 8) { stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range + stbi_uc *in = cur; + stbi_uc *out = dest; + stbi_uc inb = 0; + stbi__uint32 nsmp = x*img_n; - // note that the final byte might overshoot and write more data than desired. - // we can allocate enough data that this never writes out of memory, but it - // could also overwrite the next scanline. can it overwrite non-empty data - // on the next scanline? yes, consider 1-pixel-wide scanlines with 1-bit-per-pixel. - // so we need to explicitly clamp the final ones - + // expand bits to bytes first if (depth == 4) { - for (k=x*img_n; k >= 2; k-=2, ++in) { - *cur++ = scale * ((*in >> 4) ); - *cur++ = scale * ((*in ) & 0x0f); + for (i=0; i < nsmp; ++i) { + if ((i & 1) == 0) inb = *in++; + *out++ = scale * (inb >> 4); + inb <<= 4; } - if (k > 0) *cur++ = scale * ((*in >> 4) ); } else if (depth == 2) { - for (k=x*img_n; k >= 4; k-=4, ++in) { - *cur++ = scale * ((*in >> 6) ); - *cur++ = scale * ((*in >> 4) & 0x03); - *cur++ = scale * ((*in >> 2) & 0x03); - *cur++ = scale * ((*in ) & 0x03); + for (i=0; i < nsmp; ++i) { + if ((i & 3) == 0) inb = *in++; + *out++ = scale * (inb >> 6); + inb <<= 2; } - if (k > 0) *cur++ = scale * ((*in >> 6) ); - if (k > 1) *cur++ = scale * ((*in >> 4) & 0x03); - if (k > 2) *cur++ = scale * ((*in >> 2) & 0x03); - } else if (depth == 1) { - for (k=x*img_n; k >= 8; k-=8, ++in) { - *cur++ = scale * ((*in >> 7) ); - *cur++ = scale * ((*in >> 6) & 0x01); - *cur++ = scale * ((*in >> 5) & 0x01); - *cur++ = scale * ((*in >> 4) & 0x01); - *cur++ = scale * ((*in >> 3) & 0x01); - *cur++ = scale * ((*in >> 2) & 0x01); - *cur++ = scale * ((*in >> 1) & 0x01); - *cur++ = scale * ((*in ) & 0x01); + } else { + STBI_ASSERT(depth == 1); + for (i=0; i < nsmp; ++i) { + if ((i & 7) == 0) inb = *in++; + *out++ = scale * (inb >> 7); + inb <<= 1; } - if (k > 0) *cur++ = scale * ((*in >> 7) ); - if (k > 1) *cur++ = scale * ((*in >> 6) & 0x01); - if (k > 2) *cur++ = scale * ((*in >> 5) & 0x01); - if (k > 3) *cur++ = scale * ((*in >> 4) & 0x01); - if (k > 4) *cur++ = scale * ((*in >> 3) & 0x01); - if (k > 5) *cur++ = scale * ((*in >> 2) & 0x01); - if (k > 6) *cur++ = scale * ((*in >> 1) & 0x01); } - if (img_n != out_n) { - int q; - // insert alpha = 255 - cur = a->out + stride*j; + + // insert alpha=255 values if desired + if (img_n != out_n) + stbi__create_png_alpha_expand8(dest, dest, x, img_n); + } else if (depth == 8) { + if (img_n == out_n) + memcpy(dest, cur, x*img_n); + else + stbi__create_png_alpha_expand8(dest, cur, x, img_n); + } else if (depth == 16) { + // convert the image data from big-endian to platform-native + stbi__uint16 *dest16 = (stbi__uint16*)dest; + stbi__uint32 nsmp = x*img_n; + + if (img_n == out_n) { + for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) + *dest16 = (cur[0] << 8) | cur[1]; + } else { + STBI_ASSERT(img_n+1 == out_n); if (img_n == 1) { - for (q=x-1; q >= 0; --q) { - cur[q*2+1] = 255; - cur[q*2+0] = cur[q]; + for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = 0xffff; } } else { STBI_ASSERT(img_n == 3); - for (q=x-1; q >= 0; --q) { - cur[q*4+3] = 255; - cur[q*4+2] = cur[q*3+2]; - cur[q*4+1] = cur[q*3+1]; - cur[q*4+0] = cur[q*3+0]; + for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = (cur[2] << 8) | cur[3]; + dest16[2] = (cur[4] << 8) | cur[5]; + dest16[3] = 0xffff; } } } } - } else if (depth == 16) { - // force the image data from big-endian to platform-native. - // this is done in a separate pass due to the decoding relying - // on the data being untouched, but could probably be done - // per-line during decode if care is taken. - stbi_uc *cur = a->out; - stbi__uint16 *cur16 = (stbi__uint16*)cur; - - for(i=0; i < x*y*out_n; ++i,cur16++,cur+=2) { - *cur16 = (cur[0] << 8) | cur[1]; - } } + STBI_FREE(filter_buf); + if (!all_ok) return 0; + return 1; } diff --git a/include/SFML/Audio/Sound.hpp b/include/SFML/Audio/Sound.hpp index 6720a56f9..ae9e3591f 100644 --- a/include/SFML/Audio/Sound.hpp +++ b/include/SFML/Audio/Sound.hpp @@ -162,6 +162,17 @@ public: //////////////////////////////////////////////////////////// void setPlayingOffset(Time timeOffset); + //////////////////////////////////////////////////////////// + /// \brief Set the effect processor to be applied to the sound + /// + /// The effect processor is a callable that will be called + /// with sound data to be processed. + /// + /// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing + /// + //////////////////////////////////////////////////////////// + void setEffectProcessor(EffectProcessor effectProcessor) override; + //////////////////////////////////////////////////////////// /// \brief Get the audio buffer attached to the sound /// diff --git a/include/SFML/Audio/SoundRecorder.hpp b/include/SFML/Audio/SoundRecorder.hpp index 50e135ebd..dd9548816 100644 --- a/include/SFML/Audio/SoundRecorder.hpp +++ b/include/SFML/Audio/SoundRecorder.hpp @@ -63,7 +63,7 @@ public: /// This function uses its own thread so that it doesn't block /// the rest of the program while the capture runs. /// Please note that only one capture can happen at the same time. - /// You can select which capture device will be used, by passing + /// You can select which capture device will be used by passing /// the name to the setDevice() method. If none was selected /// before, the default capture device will be used. You can get a /// list of the names of all available capture devices by calling diff --git a/include/SFML/Audio/SoundSource.hpp b/include/SFML/Audio/SoundSource.hpp index 2b9aa701b..c6231d0f7 100644 --- a/include/SFML/Audio/SoundSource.hpp +++ b/include/SFML/Audio/SoundSource.hpp @@ -32,6 +32,8 @@ #include #include +#include + namespace sf { @@ -72,6 +74,76 @@ public: float outerGain{}; //!< Outer gain }; + //////////////////////////////////////////////////////////// + /// \brief Callable that is provided with sound data for processing + /// + /// When the audio engine sources sound data from sound + /// sources it will pass the data through an effects + /// processor if one is set. The sound data will already be + /// converted to the internal floating point format. + /// + /// Sound data that is processed this way is provided in + /// frames. Each frame contains 1 floating point sample per + /// channel. If e.g. the data source provides stereo data, + /// each frame will contain 2 floats. + /// + /// The effects processor function takes 4 parameters: + /// - The input data frames, channels interleaved + /// - The number of input data frames available + /// - The buffer to write output data frames to, channels interleaved + /// - The number of output data frames that the output buffer can hold + /// - The channel count + /// + /// The input and output frame counts are in/out parameters. + /// + /// When this function is called, the input count will + /// contain the number of frames available in the input + /// buffer. The output count will contain the size of the + /// output buffer i.e. the maximum number of frames that + /// can be written to the output buffer. + /// + /// Attempting to read more frames than the input frame + /// count or write more frames than the output frame count + /// will result in undefined behaviour. + /// + /// When done processing the frames, the input and output + /// frame counts must be updated to reflect the actual + /// number of frames that were read from the input and + /// written to the output. + /// + /// The processing function should always try to process as + /// much sound data as possible i.e. always try to fill the + /// output buffer to the maximum. In certain situations for + /// specific effects it can be possible that the input frame + /// count and output frame count aren't equal. As long as + /// the frame counts are updated accordingly this is + /// perfectly valid. + /// + /// If the audio engine determines that no audio data is + /// available from the data source, the input data frames + /// pointer is set to nullptr and the input frame count is + /// set to 0. In this case it is up to the function to + /// decide how to handle the situation. For specific effects + /// e.g. Echo/Delay buffered data might still be able to be + /// written to the output buffer even if there is no longer + /// any input data. + /// + /// An important thing to remember is that this function is + /// directly called by the audio engine. Because the audio + /// engine runs on an internal thread of its own, make sure + /// access to shared data is synchronized appropriately. + /// + /// Because this function is stored by the SoundSource + /// object it will be able to be called as long as the + /// SoundSource object hasn't yet been destroyed. Make sure + /// that any data this function references outlives the + /// SoundSource object otherwise use-after-free errors will + /// occur. + /// + //////////////////////////////////////////////////////////// + using EffectProcessor = std::function< + void(const float* inputFrames, unsigned int& inputFrameCount, float* outputFrames, unsigned int& outputFrameCount, unsigned int frameChannelCount)>; + //////////////////////////////////////////////////////////// /// \brief Copy constructor /// @@ -329,6 +401,17 @@ public: //////////////////////////////////////////////////////////// void setAttenuation(float attenuation); + //////////////////////////////////////////////////////////// + /// \brief Set the effect processor to be applied to the sound + /// + /// The effect processor is a callable that will be called + /// with sound data to be processed. + /// + /// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing + /// + //////////////////////////////////////////////////////////// + virtual void setEffectProcessor(EffectProcessor effectProcessor); + //////////////////////////////////////////////////////////// /// \brief Get the pitch of the sound /// diff --git a/include/SFML/Audio/SoundStream.hpp b/include/SFML/Audio/SoundStream.hpp index 12c29353b..87ff8240c 100644 --- a/include/SFML/Audio/SoundStream.hpp +++ b/include/SFML/Audio/SoundStream.hpp @@ -194,6 +194,17 @@ public: //////////////////////////////////////////////////////////// bool getLoop() const; + //////////////////////////////////////////////////////////// + /// \brief Set the effect processor to be applied to the sound + /// + /// The effect processor is a callable that will be called + /// with sound data to be processed. + /// + /// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing + /// + //////////////////////////////////////////////////////////// + void setEffectProcessor(EffectProcessor effectProcessor) override; + protected: //////////////////////////////////////////////////////////// /// \brief Default constructor diff --git a/include/SFML/Graphics/RenderTexture.hpp b/include/SFML/Graphics/RenderTexture.hpp index 95fdb9bea..7487011f5 100644 --- a/include/SFML/Graphics/RenderTexture.hpp +++ b/include/SFML/Graphics/RenderTexture.hpp @@ -100,11 +100,15 @@ public: /// Before calling this function, the render-texture is in /// an invalid state, thus it is mandatory to call it before /// doing anything with the render-texture. + /// /// The last parameter, \a settings, is useful if you want to enable /// multi-sampling or use the render-texture for OpenGL rendering that /// requires a depth or stencil buffer. Otherwise it is unnecessary, and /// you should leave this parameter at its default value. /// + /// After creation, the contents of the render-texture are undefined. + /// Call `RenderTexture::clear` first to ensure a single color fill. + /// /// \param size Width and height of the render-texture /// \param settings Additional settings for the underlying OpenGL texture and context /// diff --git a/include/SFML/Graphics/Texture.hpp b/include/SFML/Graphics/Texture.hpp index 5f1c84527..456a8c515 100644 --- a/include/SFML/Graphics/Texture.hpp +++ b/include/SFML/Graphics/Texture.hpp @@ -261,7 +261,7 @@ public: /// the \a area rectangle, and to contain 32-bits RGBA pixels. /// /// No additional check is performed on the size of the pixel - /// array, passing invalid arguments will lead to an undefined + /// array. Passing invalid arguments will lead to an undefined /// behavior. /// /// This function does nothing if \a pixels is null or if the @@ -279,7 +279,7 @@ public: /// \a height arguments, and it must contain 32-bits RGBA pixels. /// /// No additional check is performed on the size of the pixel - /// array or the bounds of the area to update, passing invalid + /// array or the bounds of the area to update. Passing invalid /// arguments will lead to an undefined behavior. /// /// This function does nothing if \a pixels is null or if the @@ -297,11 +297,12 @@ public: /// /// Although the source texture can be smaller than this texture, /// this function is usually used for updating the whole texture. - /// The other overload, which has (x, y) additional arguments, - /// is more convenient for updating a sub-area of this texture. + /// The other overload, which has an additional destination + /// argument, is more convenient for updating a sub-area of this + /// texture. /// /// No additional check is performed on the size of the passed - /// texture, passing a texture bigger than this texture + /// texture. Passing a texture bigger than this texture /// will lead to an undefined behavior. /// /// This function does nothing if either texture was not @@ -315,8 +316,8 @@ public: //////////////////////////////////////////////////////////// /// \brief Update a part of this texture from another texture /// - /// No additional check is performed on the size of the texture, - /// passing an invalid combination of texture size and destination + /// No additional check is performed on the size of the texture. + /// Passing an invalid combination of texture size and destination /// will lead to an undefined behavior. /// /// This function does nothing if either texture was not @@ -333,11 +334,12 @@ public: /// /// Although the source image can be smaller than the texture, /// this function is usually used for updating the whole texture. - /// The other overload, which has (x, y) additional arguments, - /// is more convenient for updating a sub-area of the texture. + /// The other overload, which has an additional destination + /// argument, is more convenient for updating a sub-area of the + /// texture. /// - /// No additional check is performed on the size of the image, - /// passing an image bigger than the texture will lead to an + /// No additional check is performed on the size of the image. + /// Passing an image bigger than the texture will lead to an /// undefined behavior. /// /// This function does nothing if the texture was not @@ -351,8 +353,8 @@ public: //////////////////////////////////////////////////////////// /// \brief Update a part of the texture from an image /// - /// No additional check is performed on the size of the image, - /// passing an invalid combination of image size and destination + /// No additional check is performed on the size of the image. + /// Passing an invalid combination of image size and destination /// will lead to an undefined behavior. /// /// This function does nothing if the texture was not @@ -369,11 +371,12 @@ public: /// /// Although the source window can be smaller than the texture, /// this function is usually used for updating the whole texture. - /// The other overload, which has (x, y) additional arguments, - /// is more convenient for updating a sub-area of the texture. + /// The other overload, which has an additional destination + /// argument, is more convenient for updating a sub-area of the + /// texture. /// - /// No additional check is performed on the size of the window, - /// passing a window bigger than the texture will lead to an + /// No additional check is performed on the size of the window. + /// Passing a window bigger than the texture will lead to an /// undefined behavior. /// /// This function does nothing if either the texture or the window @@ -387,8 +390,8 @@ public: //////////////////////////////////////////////////////////// /// \brief Update a part of the texture from the contents of a window /// - /// No additional check is performed on the size of the window, - /// passing an invalid combination of window size and destination + /// No additional check is performed on the size of the window. + /// Passing an invalid combination of window size and destination /// will lead to an undefined behavior. /// /// This function does nothing if either the texture or the window diff --git a/include/SFML/Graphics/Transform.hpp b/include/SFML/Graphics/Transform.hpp index 8ccf091fe..8d9182462 100644 --- a/include/SFML/Graphics/Transform.hpp +++ b/include/SFML/Graphics/Transform.hpp @@ -33,6 +33,8 @@ #include +#include + namespace sf { @@ -267,10 +269,10 @@ private: // Member data //////////////////////////////////////////////////////////// // clang-format off - float m_matrix[16]{1.f, 0.f, 0.f, 0.f, - 0.f, 1.f, 0.f, 0.f, - 0.f, 0.f, 1.f, 0.f, - 0.f, 0.f, 0.f, 1.f}; //!< 4x4 matrix defining the transformation + std::array m_matrix{1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f}; //!< 4x4 matrix defining the transformation // clang-format off }; diff --git a/include/SFML/Graphics/Transform.inl b/include/SFML/Graphics/Transform.inl index 945c1d615..6aee63b2d 100644 --- a/include/SFML/Graphics/Transform.inl +++ b/include/SFML/Graphics/Transform.inl @@ -55,7 +55,7 @@ constexpr Transform::Transform(float a00, float a01, float a02, //////////////////////////////////////////////////////////// constexpr const float* Transform::getMatrix() const { - return m_matrix; + return m_matrix.data(); } @@ -133,8 +133,8 @@ constexpr FloatRect Transform::transformRect(const FloatRect& rectangle) const //////////////////////////////////////////////////////////// constexpr Transform& Transform::combine(const Transform& transform) { - const float* a = m_matrix; - const float* b = transform.m_matrix; + const auto& a = m_matrix; + const auto& b = transform.m_matrix; // clang-format off *this = Transform(a[0] * b[0] + a[4] * b[1] + a[12] * b[3], diff --git a/include/SFML/Graphics/VertexBuffer.hpp b/include/SFML/Graphics/VertexBuffer.hpp index 5b01996aa..510a3e182 100644 --- a/include/SFML/Graphics/VertexBuffer.hpp +++ b/include/SFML/Graphics/VertexBuffer.hpp @@ -154,7 +154,7 @@ public: /// the \a created buffer. /// /// No additional check is performed on the size of the vertex - /// array, passing invalid arguments will lead to undefined + /// array. Passing invalid arguments will lead to undefined /// behavior. /// /// This function does nothing if \a vertices is null or if the @@ -188,7 +188,7 @@ public: /// than the size of the currently created buffer, the update fails. /// /// No additional check is performed on the size of the vertex - /// array, passing invalid arguments will lead to undefined + /// array. Passing invalid arguments will lead to undefined /// behavior. /// /// \param vertices Array of vertices to copy to the buffer @@ -386,7 +386,7 @@ SFML_GRAPHICS_API void swap(VertexBuffer& left, VertexBuffer& right) noexcept; /// Simultaneous updates to the vertex buffer are not guaranteed to be /// carried out by the driver in any specific order. Updating the same /// region of the buffer from multiple threads will not cause undefined -/// behaviour, however the final state of the buffer will be unpredictable. +/// behavior, however the final state of the buffer will be unpredictable. /// /// Simultaneous updates of distinct non-overlapping regions of the buffer /// are also not guaranteed to complete in a specific order. However, in diff --git a/include/SFML/System/Angle.hpp b/include/SFML/System/Angle.hpp index 2af146632..12aa32ed2 100644 --- a/include/SFML/System/Angle.hpp +++ b/include/SFML/System/Angle.hpp @@ -144,20 +144,20 @@ private: friend constexpr Angle radians(float angle); //////////////////////////////////////////////////////////// - /// \brief Construct from a number of degrees + /// \brief Construct from a number of radians /// /// This function is internal. To construct angle values, /// use sf::radians or sf::degrees instead. /// - /// \param degrees Angle in degrees + /// \param radians Angle in radians /// //////////////////////////////////////////////////////////// - constexpr explicit Angle(float degrees); + constexpr explicit Angle(float radians); //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - float m_degrees{}; //!< Angle value stored as degrees + float m_radians{}; //!< Angle value stored as radians }; //////////////////////////////////////////////////////////// @@ -187,6 +187,7 @@ private: //////////////////////////////////////////////////////////// /// \relates Angle /// \brief Overload of == operator to compare two angle values +/// \note Does not automatically wrap the angle value /// /// \param left Left operand (an angle) /// \param right Right operand (an angle) @@ -199,6 +200,7 @@ private: //////////////////////////////////////////////////////////// /// \relates Angle /// \brief Overload of != operator to compare two angle values +/// \note Does not automatically wrap the angle value /// /// \param left Left operand (an angle) /// \param right Right operand (an angle) @@ -211,6 +213,7 @@ private: //////////////////////////////////////////////////////////// /// \relates Angle /// \brief Overload of < operator to compare two angle values +/// \note Does not automatically wrap the angle value /// /// \param left Left operand (an angle) /// \param right Right operand (an angle) @@ -223,6 +226,7 @@ private: //////////////////////////////////////////////////////////// /// \relates Angle /// \brief Overload of > operator to compare two angle values +/// \note Does not automatically wrap the angle value /// /// \param left Left operand (an angle) /// \param right Right operand (an angle) @@ -235,6 +239,7 @@ private: //////////////////////////////////////////////////////////// /// \relates Angle /// \brief Overload of <= operator to compare two angle values +/// \note Does not automatically wrap the angle value /// /// \param left Left operand (an angle) /// \param right Right operand (an angle) @@ -247,6 +252,7 @@ private: //////////////////////////////////////////////////////////// /// \relates Angle /// \brief Overload of >= operator to compare two angle values +/// \note Does not automatically wrap the angle value /// /// \param left Left operand (an angle) /// \param right Right operand (an angle) diff --git a/include/SFML/System/Angle.inl b/include/SFML/System/Angle.inl index 53b41d526..0a6abbc64 100644 --- a/include/SFML/System/Angle.inl +++ b/include/SFML/System/Angle.inl @@ -34,16 +34,14 @@ namespace sf { namespace priv { -constexpr float pi = 3.141592654f; +constexpr float pi = 3.141592654f; +constexpr float tau = pi * 2.f; constexpr float positiveRemainder(float a, float b) { - assert(b > 0.0f && "Cannot calculate remainder with non-positive divisor"); + assert(b > 0.f && "Cannot calculate remainder with non-positive divisor"); const float val = a - static_cast(static_cast(a / b)) * b; - if (val >= 0.f) - return val; - else - return val + b; + return val >= 0.f ? val : val + b; } } // namespace priv @@ -55,33 +53,33 @@ constexpr Angle::Angle() = default; //////////////////////////////////////////////////////////// constexpr float Angle::asDegrees() const { - return m_degrees; + return m_radians * (180.f / priv::pi); } //////////////////////////////////////////////////////////// constexpr float Angle::asRadians() const { - return m_degrees * (priv::pi / 180); + return m_radians; } //////////////////////////////////////////////////////////// constexpr Angle Angle::wrapSigned() const { - return degrees(priv::positiveRemainder(m_degrees + 180, 360) - 180); + return radians(priv::positiveRemainder(m_radians + priv::pi, priv::tau) - priv::pi); } //////////////////////////////////////////////////////////// constexpr Angle Angle::wrapUnsigned() const { - return degrees(priv::positiveRemainder(m_degrees, 360)); + return radians(priv::positiveRemainder(m_radians, priv::tau)); } //////////////////////////////////////////////////////////// -constexpr Angle::Angle(float degrees) : m_degrees(degrees) +constexpr Angle::Angle(float radians) : m_radians(radians) { } @@ -89,70 +87,70 @@ constexpr Angle::Angle(float degrees) : m_degrees(degrees) //////////////////////////////////////////////////////////// constexpr Angle degrees(float angle) { - return Angle(angle); + return Angle(angle * (priv::pi / 180.f)); } //////////////////////////////////////////////////////////// constexpr Angle radians(float angle) { - return Angle(angle * (180 / priv::pi)); + return Angle(angle); } //////////////////////////////////////////////////////////// constexpr bool operator==(Angle left, Angle right) { - return left.asDegrees() == right.asDegrees(); + return left.asRadians() == right.asRadians(); } //////////////////////////////////////////////////////////// constexpr bool operator!=(Angle left, Angle right) { - return left.asDegrees() != right.asDegrees(); + return left.asRadians() != right.asRadians(); } //////////////////////////////////////////////////////////// constexpr bool operator<(Angle left, Angle right) { - return left.asDegrees() < right.asDegrees(); + return left.asRadians() < right.asRadians(); } //////////////////////////////////////////////////////////// constexpr bool operator>(Angle left, Angle right) { - return left.asDegrees() > right.asDegrees(); + return left.asRadians() > right.asRadians(); } //////////////////////////////////////////////////////////// constexpr bool operator<=(Angle left, Angle right) { - return left.asDegrees() <= right.asDegrees(); + return left.asRadians() <= right.asRadians(); } //////////////////////////////////////////////////////////// constexpr bool operator>=(Angle left, Angle right) { - return left.asDegrees() >= right.asDegrees(); + return left.asRadians() >= right.asRadians(); } //////////////////////////////////////////////////////////// constexpr Angle operator-(Angle right) { - return degrees(-right.asDegrees()); + return radians(-right.asRadians()); } //////////////////////////////////////////////////////////// constexpr Angle operator+(Angle left, Angle right) { - return degrees(left.asDegrees() + right.asDegrees()); + return radians(left.asRadians() + right.asRadians()); } @@ -166,7 +164,7 @@ constexpr Angle& operator+=(Angle& left, Angle right) //////////////////////////////////////////////////////////// constexpr Angle operator-(Angle left, Angle right) { - return degrees(left.asDegrees() - right.asDegrees()); + return radians(left.asRadians() - right.asRadians()); } @@ -180,7 +178,7 @@ constexpr Angle& operator-=(Angle& left, Angle right) //////////////////////////////////////////////////////////// constexpr Angle operator*(Angle left, float right) { - return degrees(left.asDegrees() * right); + return radians(left.asRadians() * right); } @@ -201,15 +199,15 @@ constexpr Angle& operator*=(Angle& left, float right) //////////////////////////////////////////////////////////// constexpr Angle operator/(Angle left, float right) { - assert(right != 0 && "Angle::operator/ cannot divide by 0"); - return degrees(left.asDegrees() / right); + assert(right != 0.f && "Angle::operator/ cannot divide by 0"); + return radians(left.asRadians() / right); } //////////////////////////////////////////////////////////// constexpr Angle& operator/=(Angle& left, float right) { - assert(right != 0 && "Angle::operator/= cannot divide by 0"); + assert(right != 0.f && "Angle::operator/= cannot divide by 0"); return left = left / right; } @@ -217,23 +215,23 @@ constexpr Angle& operator/=(Angle& left, float right) //////////////////////////////////////////////////////////// constexpr float operator/(Angle left, Angle right) { - assert(right.asDegrees() != 0 && "Angle::operator/ cannot divide by 0"); - return left.asDegrees() / right.asDegrees(); + assert(right.asRadians() != 0.f && "Angle::operator/ cannot divide by 0"); + return left.asRadians() / right.asRadians(); } //////////////////////////////////////////////////////////// constexpr Angle operator%(Angle left, Angle right) { - assert(right.asDegrees() != 0 && "Angle::operator% cannot modulus by 0"); - return degrees(priv::positiveRemainder(left.asDegrees(), right.asDegrees())); + assert(right.asRadians() != 0.f && "Angle::operator% cannot modulus by 0"); + return radians(priv::positiveRemainder(left.asRadians(), right.asRadians())); } //////////////////////////////////////////////////////////// constexpr Angle& operator%=(Angle& left, Angle right) { - assert(right.asDegrees() != 0 && "Angle::operator%= cannot modulus by 0"); + assert(right.asRadians() != 0.f && "Angle::operator%= cannot modulus by 0"); return left = left % right; } diff --git a/include/SFML/System/FileInputStream.hpp b/include/SFML/System/FileInputStream.hpp index 20899ba97..edbb83ed9 100644 --- a/include/SFML/System/FileInputStream.hpp +++ b/include/SFML/System/FileInputStream.hpp @@ -147,8 +147,8 @@ private: // Member data //////////////////////////////////////////////////////////// #ifdef SFML_SYSTEM_ANDROID - std::unique_ptr m_file; -#else + std::unique_ptr m_androidFile; +#endif //////////////////////////////////////////////////////////// /// \brief Deleter for stdio file stream that closes the file stream /// @@ -159,7 +159,6 @@ private: }; std::unique_ptr m_file; //!< stdio file stream -#endif }; } // namespace sf diff --git a/include/SFML/System/Utf.hpp b/include/SFML/System/Utf.hpp index f0759f2ec..8773af89d 100644 --- a/include/SFML/System/Utf.hpp +++ b/include/SFML/System/Utf.hpp @@ -29,6 +29,7 @@ //////////////////////////////////////////////////////////// #include +#include #include #include diff --git a/include/SFML/System/Utf.inl b/include/SFML/System/Utf.inl index be072f6e5..4bdf48632 100644 --- a/include/SFML/System/Utf.inl +++ b/include/SFML/System/Utf.inl @@ -56,7 +56,7 @@ In Utf<8>::decode(In begin, In end, std::uint32_t& output, std::uint32_t replace { // clang-format off // Some useful precomputed data - static constexpr int trailing[256] = + static constexpr std::array trailing = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -68,14 +68,14 @@ In Utf<8>::decode(In begin, In end, std::uint32_t& output, std::uint32_t replace 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5 }; - static constexpr std::uint32_t offsets[6] = + static constexpr std::array offsets = { 0x00000000, 0x00003080, 0x000E2080, 0x03C82080, 0xFA082080, 0x82082080 }; // clang-format on // decode the character - const int trailingBytes = trailing[static_cast(*begin)]; + const auto trailingBytes = trailing[static_cast(*begin)]; if (trailingBytes < std::distance(begin, end)) { output = 0; @@ -110,7 +110,7 @@ template Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement) { // Some useful precomputed data - static constexpr std::uint8_t firstBytes[7] = {0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC}; + static constexpr std::array firstBytes = {0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC}; // encode the character if ((input > 0x0010FFFF) || ((input >= 0xD800) && (input <= 0xDBFF))) @@ -134,7 +134,7 @@ Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement) // clang-format on // Extract the bytes to write - std::byte bytes[4]; + std::array bytes{}; // clang-format off switch (bytestoWrite) @@ -147,7 +147,7 @@ Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement) // clang-format on // Add them to the output - output = priv::copy(bytes, bytes + bytestoWrite, output); + output = priv::copy(bytes.data(), bytes.data() + bytestoWrite, output); } return output; diff --git a/include/SFML/Window/Cursor.hpp b/include/SFML/Window/Cursor.hpp index 11510485d..e721f01dc 100644 --- a/include/SFML/Window/Cursor.hpp +++ b/include/SFML/Window/Cursor.hpp @@ -240,7 +240,7 @@ private: /// with either loadFromPixels() or loadFromSystem(), the /// cursor can be changed with sf::WindowBase::setMouseCursor(). /// -/// The behaviour is undefined if the cursor is destroyed while +/// The behavior is undefined if the cursor is destroyed while /// in use by the window. /// /// Usage example: diff --git a/include/SFML/Window/Event.hpp b/include/SFML/Window/Event.hpp index e73515c73..627144915 100644 --- a/include/SFML/Window/Event.hpp +++ b/include/SFML/Window/Event.hpp @@ -329,10 +329,10 @@ private: static constexpr bool isEventType = isInParameterPack(decltype(m_data)()); }; -#include - } // namespace sf +#include + //////////////////////////////////////////////////////////// /// \class sf::Event diff --git a/include/SFML/Window/Event.inl b/include/SFML/Window/Event.inl index eded3de39..ef62cac1e 100644 --- a/include/SFML/Window/Event.inl +++ b/include/SFML/Window/Event.inl @@ -30,6 +30,14 @@ // to compile the code within the compiletime conditional when // an incorrect template parameter is provided. +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include // NOLINT(misc-header-include-cycle) + + +namespace sf +{ //////////////////////////////////////////////////////////// template Event::Event(const T& t) @@ -58,3 +66,5 @@ const T* Event::getIf() const if constexpr (isEventType) return std::get_if(&m_data); } + +} // namespace sf diff --git a/include/SFML/Window/Sensor.hpp b/include/SFML/Window/Sensor.hpp index 7662b4650..123ef358c 100644 --- a/include/SFML/Window/Sensor.hpp +++ b/include/SFML/Window/Sensor.hpp @@ -44,11 +44,11 @@ namespace sf::Sensor enum class Type { Accelerometer, //!< Measures the raw acceleration (m/s^2) - Gyroscope, //!< Measures the raw rotation rates (degrees/s) + Gyroscope, //!< Measures the raw rotation rates (radians/s) Magnetometer, //!< Measures the ambient magnetic field (micro-teslas) Gravity, //!< Measures the direction and intensity of gravity, independent of device acceleration (m/s^2) UserAcceleration, //!< Measures the direction and intensity of device acceleration, independent of the gravity (m/s^2) - Orientation //!< Measures the absolute 3D orientation (degrees) + Orientation //!< Measures the absolute 3D orientation (radians) }; // NOLINTNEXTLINE(readability-identifier-naming) diff --git a/src/SFML/Audio/AudioDevice.cpp b/src/SFML/Audio/AudioDevice.cpp index b18c5ee33..dc2c6a145 100644 --- a/src/SFML/Audio/AudioDevice.cpp +++ b/src/SFML/Audio/AudioDevice.cpp @@ -30,8 +30,12 @@ #include #include +#include #include +#include +#include + namespace sf::priv { @@ -64,33 +68,52 @@ AudioDevice::AudioDevice() // Create the context m_context.emplace(); - auto contextConfig = ma_context_config_init(); - contextConfig.pLog = &*m_log; + auto contextConfig = ma_context_config_init(); + contextConfig.pLog = &*m_log; + ma_uint32 deviceCount = 0; + const auto nullBackend = ma_backend_null; + const std::array backendLists{nullptr, &nullBackend}; - if (const auto result = ma_context_init(nullptr, 0, &contextConfig, &*m_context); result != MA_SUCCESS) + for (const auto* backendList : backendLists) { - m_context.reset(); - err() << "Failed to initialize the audio context: " << ma_result_description(result) << std::endl; - return; + // We can set backendCount to 1 since it is ignored when backends is set to nullptr + if (const auto result = ma_context_init(backendList, 1, &contextConfig, &*m_context); result != MA_SUCCESS) + { + m_context.reset(); + err() << "Failed to initialize the audio playback context: " << ma_result_description(result) << std::endl; + return; + } + + // Count the playback devices + if (const auto result = ma_context_get_devices(&*m_context, nullptr, &deviceCount, nullptr, nullptr); + result != MA_SUCCESS) + { + err() << "Failed to get audio playback devices: " << ma_result_description(result) << std::endl; + return; + } + + // Check if there are audio playback devices available on the system + if (deviceCount > 0) + break; + + // Warn if no devices were found using the default backend list + if (backendList == nullptr) + err() << "No audio playback devices available on the system" << std::endl; + + // Clean up the context if we didn't find any devices + ma_context_uninit(&*m_context); } - // Count the playback devices - ma_uint32 deviceCount = 0; - - if (const auto result = ma_context_get_devices(&*m_context, nullptr, &deviceCount, nullptr, nullptr); - result != MA_SUCCESS) - { - err() << "Failed to get audio playback devices: " << ma_result_description(result) << std::endl; - return; - } - - // Check if there are audio playback devices available on the system + // If the NULL audio backend also doesn't provide a device we give up if (deviceCount == 0) { - err() << "No audio playback devices available on the system" << std::endl; + m_context.reset(); return; } + if (m_context->backend == ma_backend_null) + err() << "Using NULL audio backend for playback" << std::endl; + // Create the playback device m_playbackDevice.emplace(); @@ -159,11 +182,16 @@ AudioDevice::AudioDevice() m_listenerProperties.upVector.x, m_listenerProperties.upVector.y, m_listenerProperties.upVector.z); + + // Setup cleanup function with `atexit` to work around destruction order issue with + // miniaudio internal audio processing threads on Windows + const int rc = std::atexit([] { AudioDevice::get().cleanup(); }); + assert(rc == 0); } //////////////////////////////////////////////////////////// -AudioDevice::~AudioDevice() +void AudioDevice::cleanup() { // Destroy the engine if (m_engine) @@ -286,8 +314,8 @@ void AudioDevice::setCone(const Listener::Cone& cone) ma_engine_listener_set_cone(&*m_engine, 0, - std::clamp(cone.innerAngle.asRadians(), 0.f, sf::degrees(360).asRadians()), - std::clamp(cone.outerAngle.asRadians(), 0.f, sf::degrees(360).asRadians()), + std::clamp(cone.innerAngle, Angle::Zero, degrees(360.f)).asRadians(), + std::clamp(cone.outerAngle, Angle::Zero, degrees(360.f)).asRadians(), cone.outerGain); } diff --git a/src/SFML/Audio/AudioDevice.hpp b/src/SFML/Audio/AudioDevice.hpp index 39a17cc9b..ca18b35d9 100644 --- a/src/SFML/Audio/AudioDevice.hpp +++ b/src/SFML/Audio/AudioDevice.hpp @@ -54,10 +54,10 @@ private: AudioDevice(); //////////////////////////////////////////////////////////// - /// \brief Destructor + /// \brief Cleanup function, called via `atexit` /// //////////////////////////////////////////////////////////// - ~AudioDevice(); + void cleanup(); public: //////////////////////////////////////////////////////////// @@ -227,11 +227,11 @@ private: struct ListenerProperties { float volume{100.f}; - sf::Vector3f position{0, 0, 0}; - sf::Vector3f direction{0, 0, -1}; - sf::Vector3f velocity{0, 0, 0}; - Listener::Cone cone{sf::degrees(360), sf::degrees(360), 1}; - sf::Vector3f upVector{0, 1, 0}; + Vector3f position{0, 0, 0}; + Vector3f direction{0, 0, -1}; + Vector3f velocity{0, 0, 0}; + Listener::Cone cone{degrees(360.f), degrees(360.f), 1}; + Vector3f upVector{0, 1, 0}; }; //////////////////////////////////////////////////////////// diff --git a/src/SFML/Audio/CMakeLists.txt b/src/SFML/Audio/CMakeLists.txt index 6e1c5d75b..6f10c31f7 100644 --- a/src/SFML/Audio/CMakeLists.txt +++ b/src/SFML/Audio/CMakeLists.txt @@ -82,7 +82,7 @@ sfml_add_library(Audio target_compile_definitions(sfml-audio PRIVATE OV_EXCLUDE_STATIC_CALLBACKS FLAC__NO_DLL) # disable miniaudio features we do not use -target_compile_definitions(sfml-audio PRIVATE MA_NO_DECODING MA_NO_ENCODING MA_NO_RESOURCE_MANAGER MA_NO_GENERATION) +target_compile_definitions(sfml-audio PRIVATE MA_NO_MP3 MA_NO_FLAC MA_NO_ENCODING MA_NO_RESOURCE_MANAGER MA_NO_GENERATION) # setup dependencies target_link_libraries(sfml-audio diff --git a/src/SFML/Audio/MiniaudioUtils.cpp b/src/SFML/Audio/MiniaudioUtils.cpp index 85c70cba2..44cd82e8a 100644 --- a/src/SFML/Audio/MiniaudioUtils.cpp +++ b/src/SFML/Audio/MiniaudioUtils.cpp @@ -63,8 +63,8 @@ struct SavedSettings float minGain{0.f}; float maxGain{1.f}; float rollOff{1.f}; - float innerAngle{sf::degrees(360).asRadians()}; - float outerAngle{sf::degrees(360).asRadians()}; + float innerAngle{degrees(360.f).asRadians()}; + float outerAngle{degrees(360.f).asRadians()}; float outerGain{0.f}; }; @@ -135,7 +135,7 @@ void initializeDataSource(ma_data_source_base& dataSourceBase, const ma_data_sou //////////////////////////////////////////////////////////// -ma_channel MiniaudioUtils::soundChannelToMiniaudioChannel(sf::SoundChannel soundChannel) +ma_channel MiniaudioUtils::soundChannelToMiniaudioChannel(SoundChannel soundChannel) { switch (soundChannel) { @@ -184,6 +184,56 @@ ma_channel MiniaudioUtils::soundChannelToMiniaudioChannel(sf::SoundChannel sound } +//////////////////////////////////////////////////////////// +SoundChannel MiniaudioUtils::miniaudioChannelToSoundChannel(ma_channel soundChannel) +{ + switch (soundChannel) + { + case MA_CHANNEL_NONE: + return SoundChannel::Unspecified; + case MA_CHANNEL_MONO: + return SoundChannel::Mono; + case MA_CHANNEL_FRONT_LEFT: + return SoundChannel::FrontLeft; + case MA_CHANNEL_FRONT_RIGHT: + return SoundChannel::FrontRight; + case MA_CHANNEL_FRONT_CENTER: + return SoundChannel::FrontCenter; + case MA_CHANNEL_FRONT_LEFT_CENTER: + return SoundChannel::FrontLeftOfCenter; + case MA_CHANNEL_FRONT_RIGHT_CENTER: + return SoundChannel::FrontRightOfCenter; + case MA_CHANNEL_LFE: + return SoundChannel::LowFrequencyEffects; + case MA_CHANNEL_BACK_LEFT: + return SoundChannel::BackLeft; + case MA_CHANNEL_BACK_RIGHT: + return SoundChannel::BackRight; + case MA_CHANNEL_BACK_CENTER: + return SoundChannel::BackCenter; + case MA_CHANNEL_SIDE_LEFT: + return SoundChannel::SideLeft; + case MA_CHANNEL_SIDE_RIGHT: + return SoundChannel::SideRight; + case MA_CHANNEL_TOP_CENTER: + return SoundChannel::TopCenter; + case MA_CHANNEL_TOP_FRONT_LEFT: + return SoundChannel::TopFrontLeft; + case MA_CHANNEL_TOP_FRONT_RIGHT: + return SoundChannel::TopFrontRight; + case MA_CHANNEL_TOP_FRONT_CENTER: + return SoundChannel::TopFrontCenter; + case MA_CHANNEL_TOP_BACK_LEFT: + return SoundChannel::TopBackLeft; + case MA_CHANNEL_TOP_BACK_RIGHT: + return SoundChannel::TopBackRight; + default: + assert(soundChannel == MA_CHANNEL_TOP_BACK_CENTER); + return SoundChannel::TopBackCenter; + } +} + + //////////////////////////////////////////////////////////// Time MiniaudioUtils::getPlayingOffset(ma_sound& sound) { diff --git a/src/SFML/Audio/MiniaudioUtils.hpp b/src/SFML/Audio/MiniaudioUtils.hpp index f5717d05b..a9d86ce63 100644 --- a/src/SFML/Audio/MiniaudioUtils.hpp +++ b/src/SFML/Audio/MiniaudioUtils.hpp @@ -41,17 +41,18 @@ namespace sf { class Time; -} -namespace sf::priv::MiniaudioUtils +namespace priv::MiniaudioUtils { -[[nodiscard]] ma_channel soundChannelToMiniaudioChannel(sf::SoundChannel soundChannel); -[[nodiscard]] Time getPlayingOffset(ma_sound& sound); -[[nodiscard]] ma_uint64 getFrameIndex(ma_sound& sound, Time timeOffset); +[[nodiscard]] ma_channel soundChannelToMiniaudioChannel(SoundChannel soundChannel); +[[nodiscard]] SoundChannel miniaudioChannelToSoundChannel(ma_channel soundChannel); +[[nodiscard]] Time getPlayingOffset(ma_sound& sound); +[[nodiscard]] ma_uint64 getFrameIndex(ma_sound& sound, Time timeOffset); void reinitializeSound(ma_sound& sound, const std::function& initializeFn); void initializeSound(const ma_data_source_vtable& vtable, ma_data_source_base& dataSourceBase, ma_sound& sound, const std::function& initializeFn); -} // namespace sf::priv::MiniaudioUtils +} // namespace priv::MiniaudioUtils +} // namespace sf diff --git a/src/SFML/Audio/Sound.cpp b/src/SFML/Audio/Sound.cpp index 5e71b831b..a656fef26 100644 --- a/src/SFML/Audio/Sound.cpp +++ b/src/SFML/Audio/Sound.cpp @@ -55,6 +55,7 @@ struct Sound::Impl ~Impl() { ma_sound_uninit(&sound); + ma_node_uninit(&effectNode, nullptr); ma_data_source_uninit(&dataSourceBase); } @@ -90,6 +91,34 @@ struct Sound::Impl return; } + // Initialize the custom effect node + effectNodeVTable.onProcess = + [](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut) + { static_cast(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); }; + effectNodeVTable.onGetRequiredInputFrameCount = nullptr; + effectNodeVTable.inputBusCount = 1; + effectNodeVTable.outputBusCount = 1; + effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT; + + const auto nodeChannelCount = ma_engine_get_channels(engine); + ma_node_config nodeConfig = ma_node_config_init(); + nodeConfig.vtable = &effectNodeVTable; + nodeConfig.pInputChannels = &nodeChannelCount; + nodeConfig.pOutputChannels = &nodeChannelCount; + + if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode); + result != MA_SUCCESS) + { + err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl; + return; + } + + effectNode.impl = this; + effectNode.channelCount = nodeChannelCount; + + // Route the sound through the effect node depending on whether an effect processor is set + connectEffect(bool{effectProcessor}); + // Because we are providing a custom data source, we have to provide the channel map ourselves if (buffer && !buffer->getChannelMap().empty()) { @@ -110,7 +139,80 @@ struct Sound::Impl void reinitialize() { - priv::MiniaudioUtils::reinitializeSound(sound, [this] { initialize(); }); + priv::MiniaudioUtils::reinitializeSound(sound, + [this] + { + ma_node_uninit(&effectNode, nullptr); + initialize(); + }); + } + + void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const + { + // If a processor is set, call it + if (effectProcessor) + { + if (!framesIn) + frameCountIn = 0; + + effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount); + + return; + } + + // Otherwise just pass the data through 1:1 + if (framesIn == nullptr) + { + frameCountIn = 0; + frameCountOut = 0; + return; + } + + const auto toProcess = std::min(frameCountIn, frameCountOut); + std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float)); + frameCountIn = toProcess; + frameCountOut = toProcess; + } + + void connectEffect(bool connect) + { + auto* engine = priv::AudioDevice::get().getEngine(); + + if (engine == nullptr) + { + err() << "Failed to connect effect: No engine available" << std::endl; + return; + } + + if (connect) + { + // Attach the custom effect node output to our engine endpoint + if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0); + result != MA_SUCCESS) + { + err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl; + return; + } + } + else + { + // Detach the custom effect node output from our engine endpoint + if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS) + { + err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result) + << std::endl; + return; + } + } + + // Attach the sound output to the custom effect node or the engine endpoint + if (const ma_result + result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0); + result != MA_SUCCESS) + { + err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl; + return; + } } static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) @@ -208,13 +310,23 @@ struct Sound::Impl //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// + struct EffectNode + { + ma_node_base base{}; + Impl* impl{}; + ma_uint32 channelCount{}; + }; + ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) + ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node + EffectNode effectNode; //!< The engine node that performs effect processing std::vector soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) ma_sound sound{}; //!< The sound std::size_t cursor{}; //!< The current playing position bool looping{}; //!< True if we are looping the sound const SoundBuffer* buffer{}; //!< Sound buffer bound to the source Status status{Status::Stopped}; //!< The status + EffectProcessor effectProcessor; //!< The effect processor }; @@ -334,6 +446,14 @@ void Sound::setPlayingOffset(Time timeOffset) } +//////////////////////////////////////////////////////////// +void Sound::setEffectProcessor(EffectProcessor effectProcessor) +{ + m_impl->effectProcessor = std::move(effectProcessor); + m_impl->connectEffect(bool{m_impl->effectProcessor}); +} + + //////////////////////////////////////////////////////////// const SoundBuffer& Sound::getBuffer() const { diff --git a/src/SFML/Audio/SoundFileReaderFlac.hpp b/src/SFML/Audio/SoundFileReaderFlac.hpp index ba97637c5..0ecb52068 100644 --- a/src/SFML/Audio/SoundFileReaderFlac.hpp +++ b/src/SFML/Audio/SoundFileReaderFlac.hpp @@ -69,7 +69,7 @@ public: /// \return Properties of the loaded sound if the file was successfully opened /// //////////////////////////////////////////////////////////// - [[nodiscard]] std::optional open(sf::InputStream& stream) override; + [[nodiscard]] std::optional open(InputStream& stream) override; //////////////////////////////////////////////////////////// /// \brief Change the current read position to the given sample offset diff --git a/src/SFML/Audio/SoundFileReaderMp3.cpp b/src/SFML/Audio/SoundFileReaderMp3.cpp index 32f0dbce0..d0ca6574c 100644 --- a/src/SFML/Audio/SoundFileReaderMp3.cpp +++ b/src/SFML/Audio/SoundFileReaderMp3.cpp @@ -142,16 +142,16 @@ std::optional SoundFileReaderMp3::open(InputStream& strea switch (info.channelCount) { case 0: - sf::err() << "No channels in MP3 file" << std::endl; + err() << "No channels in MP3 file" << std::endl; break; case 1: - info.channelMap = {sf::SoundChannel::Mono}; + info.channelMap = {SoundChannel::Mono}; break; case 2: - info.channelMap = {sf::SoundChannel::SideLeft, sf::SoundChannel::SideRight}; + info.channelMap = {SoundChannel::SideLeft, SoundChannel::SideRight}; break; default: - sf::err() << "MP3 files with more than 2 channels not supported" << std::endl; + err() << "MP3 files with more than 2 channels not supported" << std::endl; assert(false); break; } diff --git a/src/SFML/Audio/SoundFileReaderWav.cpp b/src/SFML/Audio/SoundFileReaderWav.cpp index 6769f59d8..469109837 100644 --- a/src/SFML/Audio/SoundFileReaderWav.cpp +++ b/src/SFML/Audio/SoundFileReaderWav.cpp @@ -25,82 +25,64 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include #include -#include +#include #include +#include #include #include -#include namespace { -// The following functions read integers as little endian and -// return them in the host byte order - -bool decode(sf::InputStream& stream, std::uint8_t& value) +ma_result onRead(ma_decoder* decoder, void* buffer, size_t bytesToRead, size_t* bytesRead) { - return static_cast(stream.read(&value, sizeof(value))) == sizeof(value); + auto* stream = static_cast(decoder->pUserData); + const auto count = stream->read(buffer, static_cast(bytesToRead)); + + if (count < 0) + return MA_ERROR; + + *bytesRead = static_cast(count); + return MA_SUCCESS; } -bool decode(sf::InputStream& stream, std::int16_t& value) +ma_result onSeek(ma_decoder* decoder, ma_int64 byteOffset, ma_seek_origin origin) { - std::byte bytes[sizeof(value)]; - if (static_cast(stream.read(bytes, static_cast(sizeof(bytes)))) != sizeof(bytes)) - return false; + auto* stream = static_cast(decoder->pUserData); - value = sf::toInteger(bytes[0], bytes[1]); + switch (origin) + { + case ma_seek_origin_start: + { + if (stream->seek(byteOffset) < 0) + return MA_ERROR; - return true; + return MA_SUCCESS; + } + case ma_seek_origin_current: + { + const auto currentPosition = stream->tell(); + + if (currentPosition < 0) + return MA_ERROR; + + if (stream->seek(stream->tell() + byteOffset) < 0) + return MA_ERROR; + + return MA_SUCCESS; + } + // According to miniaudio comments, ma_seek_origin_end is not used by decoders + default: + return MA_ERROR; + } } - -bool decode(sf::InputStream& stream, std::uint16_t& value) -{ - std::byte bytes[sizeof(value)]; - if (static_cast(stream.read(bytes, static_cast(sizeof(bytes)))) != sizeof(bytes)) - return false; - - value = sf::toInteger(bytes[0], bytes[1]); - - return true; -} - -bool decode24bit(sf::InputStream& stream, std::uint32_t& value) -{ - std::byte bytes[3]; - if (static_cast(stream.read(bytes, static_cast(sizeof(bytes)))) != sizeof(bytes)) - return false; - - value = sf::toInteger(bytes[0], bytes[1], bytes[2]); - - return true; -} - -bool decode(sf::InputStream& stream, std::uint32_t& value) -{ - std::byte bytes[sizeof(value)]; - if (static_cast(stream.read(bytes, static_cast(sizeof(bytes)))) != sizeof(bytes)) - return false; - - value = sf::toInteger(bytes[0], bytes[1], bytes[2], bytes[3]); - - return true; -} - -const std::uint64_t mainChunkSize = 12; - -const std::uint16_t waveFormatPcm = 1; - -const std::uint16_t waveFormatExtensible = 65534; - -const char* waveSubformatPcm = - "\x01\x00\x00\x00\x00\x00\x10\x00" - "\x80\x00\x00\xAA\x00\x38\x9B\x71"; } // namespace namespace sf::priv @@ -108,331 +90,114 @@ namespace sf::priv //////////////////////////////////////////////////////////// bool SoundFileReaderWav::check(InputStream& stream) { - char header[mainChunkSize]; - if (stream.read(header, sizeof(header)) < static_cast(sizeof(header))) - return false; + auto config = ma_decoder_config_init_default(); + config.encodingFormat = ma_encoding_format_wav; + config.format = ma_format_s16; + ma_decoder decoder{}; - return (header[0] == 'R') && (header[1] == 'I') && (header[2] == 'F') && (header[3] == 'F') && (header[8] == 'W') && - (header[9] == 'A') && (header[10] == 'V') && (header[11] == 'E'); + if (ma_decoder_init(&onRead, &onSeek, &stream, &config, &decoder) == MA_SUCCESS) + { + ma_decoder_uninit(&decoder); + return true; + } + + return false; +} + + +//////////////////////////////////////////////////////////// +SoundFileReaderWav::~SoundFileReaderWav() +{ + if (m_decoder) + { + if (const ma_result result = ma_decoder_uninit(&*m_decoder); result != MA_SUCCESS) + err() << "Failed to uninitialize wav decoder: " << ma_result_description(result) << std::endl; + } } //////////////////////////////////////////////////////////// std::optional SoundFileReaderWav::open(InputStream& stream) { - m_stream = &stream; + if (m_decoder) + { + if (const ma_result result = ma_decoder_uninit(&*m_decoder); result != MA_SUCCESS) + { + err() << "Failed to uninitialize wav decoder: " << ma_result_description(result) << std::endl; + return std::nullopt; + } + } + else + { + m_decoder.emplace(); + } - auto info = parseHeader(); - if (!info) - err() << "Failed to open WAV sound file (invalid or unsupported file)" << std::endl; + auto config = ma_decoder_config_init_default(); + config.encodingFormat = ma_encoding_format_wav; + config.format = ma_format_s16; - return info; + if (const ma_result result = ma_decoder_init(&onRead, &onSeek, &stream, &config, &*m_decoder); result != MA_SUCCESS) + { + err() << "Failed to initialize wav decoder: " << ma_result_description(result) << std::endl; + m_decoder = std::nullopt; + return std::nullopt; + } + + ma_uint64 frameCount{}; + if (const ma_result result = ma_decoder_get_available_frames(&*m_decoder, &frameCount); result != MA_SUCCESS) + { + err() << "Failed to get available frames from wav decoder: " << ma_result_description(result) << std::endl; + return std::nullopt; + } + + auto format = ma_format_unknown; + ma_uint32 sampleRate{}; + std::array channelMap{}; + if (const ma_result result = ma_decoder_get_data_format(&*m_decoder, + &format, + &m_channelCount, + &sampleRate, + channelMap.data(), + channelMap.size()); + result != MA_SUCCESS) + { + err() << "Failed to get data format from wav decoder: " << ma_result_description(result) << std::endl; + return std::nullopt; + } + + std::vector soundChannels; + soundChannels.reserve(m_channelCount); + + for (auto i = 0u; i < m_channelCount; ++i) + soundChannels.emplace_back(priv::MiniaudioUtils::miniaudioChannelToSoundChannel(channelMap[i])); + + return Info{frameCount * m_channelCount, m_channelCount, sampleRate, std::move(soundChannels)}; } //////////////////////////////////////////////////////////// void SoundFileReaderWav::seek(std::uint64_t sampleOffset) { - assert(m_stream && "Input stream cannot be null. Call SoundFileReaderWav::open() to initialize it."); + assert(m_decoder && "wav decoder not initialized. Call SoundFileReaderWav::open() to initialize it."); - if (m_stream->seek(static_cast(m_dataStart + sampleOffset * m_bytesPerSample) == -1)) - err() << "Failed to seek WAV sound stream" << std::endl; + if (const ma_result result = ma_decoder_seek_to_pcm_frame(&*m_decoder, sampleOffset / m_channelCount); + result != MA_SUCCESS) + err() << "Failed to seek wav sound stream: " << ma_result_description(result) << std::endl; } //////////////////////////////////////////////////////////// std::uint64_t SoundFileReaderWav::read(std::int16_t* samples, std::uint64_t maxCount) { - assert(m_stream && "Input stream cannot be null. Call SoundFileReaderWav::open() to initialize it."); + assert(m_decoder && "wav decoder not initialized. Call SoundFileReaderWav::open() to initialize it."); - std::uint64_t count = 0; - const auto startPos = static_cast(m_stream->tell()); + ma_uint64 framesRead{}; - // Tracking of m_dataEnd is important to prevent sf::Music from reading - // data until EOF, as WAV files may have metadata at the end. - while ((count < maxCount) && (startPos + count * m_bytesPerSample < m_dataEnd)) - { - switch (m_bytesPerSample) - { - case 1: - { - std::uint8_t sample = 0; - if (decode(*m_stream, sample)) - *samples++ = static_cast((static_cast(sample) - 128) << 8); - else - return count; - break; - } + if (const ma_result result = ma_decoder_read_pcm_frames(&*m_decoder, samples, maxCount / m_channelCount, &framesRead); + result != MA_SUCCESS) + err() << "Failed to read from wav sound stream: " << ma_result_description(result) << std::endl; - case 2: - { - std::int16_t sample = 0; - if (decode(*m_stream, sample)) - *samples++ = sample; - else - return count; - break; - } - - case 3: - { - std::uint32_t sample = 0; - if (decode24bit(*m_stream, sample)) - *samples++ = static_cast(sample >> 8); - else - return count; - break; - } - - case 4: - { - std::uint32_t sample = 0; - if (decode(*m_stream, sample)) - *samples++ = static_cast(sample >> 16); - else - return count; - break; - } - - default: - { - assert(false && "Invalid bytes per sample. Must be 1, 2, 3, or 4."); - return 0; - } - } - - ++count; - } - - return count; -} - - -//////////////////////////////////////////////////////////// -std::optional SoundFileReaderWav::parseHeader() -{ - assert(m_stream && "Input stream cannot be null. Call SoundFileReaderWav::open() to initialize it."); - - // If we are here, it means that the first part of the header - // (the format) has already been checked - char mainChunk[mainChunkSize]; - if (static_cast(m_stream->read(mainChunk, static_cast(sizeof(mainChunk)))) != - sizeof(mainChunk)) - return std::nullopt; - - // Parse all the sub-chunks - Info info; - bool dataChunkFound = false; - while (!dataChunkFound) - { - // Parse the sub-chunk id and size - char subChunkId[4]; - if (static_cast(m_stream->read(subChunkId, static_cast(sizeof(subChunkId)))) != - sizeof(subChunkId)) - return std::nullopt; - std::uint32_t subChunkSize = 0; - if (!decode(*m_stream, subChunkSize)) - return std::nullopt; - const std::int64_t subChunkStart = m_stream->tell(); - if (subChunkStart == -1) - return std::nullopt; - - // Check which chunk it is - if ((subChunkId[0] == 'f') && (subChunkId[1] == 'm') && (subChunkId[2] == 't') && (subChunkId[3] == ' ')) - { - // "fmt" chunk - - // Audio format - std::uint16_t format = 0; - if (!decode(*m_stream, format)) - return std::nullopt; - if ((format != waveFormatPcm) && (format != waveFormatExtensible)) - return std::nullopt; - - // Channel count - std::uint16_t channelCount = 0; - if (!decode(*m_stream, channelCount)) - return std::nullopt; - info.channelCount = channelCount; - - // Sample rate - std::uint32_t sampleRate = 0; - if (!decode(*m_stream, sampleRate)) - return std::nullopt; - info.sampleRate = sampleRate; - - // Byte rate - std::uint32_t byteRate = 0; - if (!decode(*m_stream, byteRate)) - return std::nullopt; - - // Block align - std::uint16_t blockAlign = 0; - if (!decode(*m_stream, blockAlign)) - return std::nullopt; - - // Bits per sample - std::uint16_t bitsPerSample = 0; - if (!decode(*m_stream, bitsPerSample)) - return std::nullopt; - if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 24 && bitsPerSample != 32) - { - err() << "Unsupported sample size: " << bitsPerSample - << " bit (Supported sample sizes are 8/16/24/32 bit)" << std::endl; - return std::nullopt; - } - m_bytesPerSample = bitsPerSample / 8; - - if (format == waveFormatExtensible) - { - // Extension size - std::uint16_t extensionSize = 0; - if (!decode(*m_stream, extensionSize)) - return std::nullopt; - - // Valid bits per sample - std::uint16_t validBitsPerSample = 0; - if (!decode(*m_stream, validBitsPerSample)) - return std::nullopt; - - // Channel mask - std::uint32_t channelMask = 0; - if (!decode(*m_stream, channelMask)) - return std::nullopt; - - // NOLINTBEGIN(readability-identifier-naming) - // For WAVE channel mapping refer to: https://learn.microsoft.com/en-us/previous-versions/windows/hardware/design/dn653308(v=vs.85)#default-channel-ordering - static constexpr auto SPEAKER_FRONT_LEFT = 0x1u; - static constexpr auto SPEAKER_FRONT_RIGHT = 0x2u; - static constexpr auto SPEAKER_FRONT_CENTER = 0x4u; - static constexpr auto SPEAKER_LOW_FREQUENCY = 0x8u; - static constexpr auto SPEAKER_BACK_LEFT = 0x10u; - static constexpr auto SPEAKER_BACK_RIGHT = 0x20u; - static constexpr auto SPEAKER_FRONT_LEFT_OF_CENTER = 0x40u; - static constexpr auto SPEAKER_FRONT_RIGHT_OF_CENTER = 0x80u; - static constexpr auto SPEAKER_BACK_CENTER = 0x100u; - static constexpr auto SPEAKER_SIDE_LEFT = 0x200u; - static constexpr auto SPEAKER_SIDE_RIGHT = 0x400u; - static constexpr auto SPEAKER_TOP_CENTER = 0x800u; - static constexpr auto SPEAKER_TOP_FRONT_LEFT = 0x1000u; - static constexpr auto SPEAKER_TOP_FRONT_CENTER = 0x2000u; - static constexpr auto SPEAKER_TOP_FRONT_RIGHT = 0x4000u; - static constexpr auto SPEAKER_TOP_BACK_LEFT = 0x8000u; - static constexpr auto SPEAKER_TOP_BACK_CENTER = 0x10000u; - static constexpr auto SPEAKER_TOP_BACK_RIGHT = 0x20000u; - // NOLINTEND(readability-identifier-naming) - - info.channelMap.clear(); - - const auto checkChannel = [channelMask, &info](auto bit, auto soundChannel) - { - if ((channelMask & bit) != 0) - info.channelMap.push_back(soundChannel); - }; - - checkChannel(SPEAKER_FRONT_LEFT, SoundChannel::FrontLeft); - checkChannel(SPEAKER_FRONT_RIGHT, SoundChannel::FrontRight); - checkChannel(SPEAKER_FRONT_CENTER, SoundChannel::FrontCenter); - checkChannel(SPEAKER_LOW_FREQUENCY, SoundChannel::LowFrequencyEffects); - checkChannel(SPEAKER_BACK_LEFT, SoundChannel::BackLeft); - checkChannel(SPEAKER_BACK_RIGHT, SoundChannel::BackRight); - checkChannel(SPEAKER_FRONT_LEFT_OF_CENTER, SoundChannel::FrontLeftOfCenter); - checkChannel(SPEAKER_FRONT_RIGHT_OF_CENTER, SoundChannel::FrontRightOfCenter); - checkChannel(SPEAKER_BACK_CENTER, SoundChannel::BackCenter); - checkChannel(SPEAKER_SIDE_LEFT, SoundChannel::SideLeft); - checkChannel(SPEAKER_SIDE_RIGHT, SoundChannel::SideRight); - checkChannel(SPEAKER_TOP_CENTER, SoundChannel::TopCenter); - checkChannel(SPEAKER_TOP_FRONT_LEFT, SoundChannel::TopFrontLeft); - checkChannel(SPEAKER_TOP_FRONT_CENTER, SoundChannel::TopFrontCenter); - checkChannel(SPEAKER_TOP_FRONT_RIGHT, SoundChannel::TopFrontRight); - checkChannel(SPEAKER_TOP_BACK_LEFT, SoundChannel::TopBackLeft); - checkChannel(SPEAKER_TOP_BACK_CENTER, SoundChannel::TopBackCenter); - checkChannel(SPEAKER_TOP_BACK_RIGHT, SoundChannel::TopBackRight); - - assert(info.channelCount == info.channelMap.size()); - - if (info.channelCount != info.channelMap.size()) - { - err() << "WAV sound file channel count does not match number of set bits in channel mask" << std::endl; - return std::nullopt; - } - - // Subformat - char subformat[16]; - if (static_cast(m_stream->read(subformat, static_cast(sizeof(subformat)))) != - sizeof(subformat)) - return std::nullopt; - - if (std::memcmp(subformat, waveSubformatPcm, sizeof(subformat)) != 0) - { - err() << "Unsupported format: extensible format with non-PCM subformat" << std::endl; - return std::nullopt; - } - - if (validBitsPerSample != bitsPerSample) - { - err() << "Unsupported format: sample size (" << validBitsPerSample - << " bits) and " - "sample container size (" - << bitsPerSample << " bits) differ" << std::endl; - return std::nullopt; - } - } - else - { - // If we don't have a waveFormatExtensible header, fill the channel map based on a best guess for mono/stereo - info.channelMap.clear(); - - if (info.channelCount == 0) - { - err() << "WAV sound file channel count 0" << std::endl; - return std::nullopt; - } - else if (info.channelCount == 1) - { - info.channelMap.push_back(SoundChannel::Mono); - } - else if (info.channelCount == 2) - { - info.channelMap.push_back(SoundChannel::FrontLeft); - info.channelMap.push_back(SoundChannel::FrontRight); - } - else - { - info.channelMap.push_back(SoundChannel::FrontLeft); - info.channelMap.push_back(SoundChannel::FrontRight); - - for (auto i = 2u; i < info.channelCount; ++i) - info.channelMap.push_back(SoundChannel::Unspecified); - } - } - - // Skip potential extra information - if (m_stream->seek(subChunkStart + subChunkSize) == -1) - return std::nullopt; - } - else if ((subChunkId[0] == 'd') && (subChunkId[1] == 'a') && (subChunkId[2] == 't') && (subChunkId[3] == 'a')) - { - // "data" chunk - - // Compute the total number of samples - info.sampleCount = subChunkSize / m_bytesPerSample; - - // Store the start and end position of samples in the file - m_dataStart = static_cast(subChunkStart); - m_dataEnd = m_dataStart + info.sampleCount * m_bytesPerSample; - - dataChunkFound = true; - } - else - { - // unknown chunk, skip it - if (m_stream->seek(m_stream->tell() + subChunkSize) == -1) - return std::nullopt; - } - } - - return info; + return framesRead * m_channelCount; } } // namespace sf::priv diff --git a/src/SFML/Audio/SoundFileReaderWav.hpp b/src/SFML/Audio/SoundFileReaderWav.hpp index 479f350b0..d666897bc 100644 --- a/src/SFML/Audio/SoundFileReaderWav.hpp +++ b/src/SFML/Audio/SoundFileReaderWav.hpp @@ -29,6 +29,8 @@ //////////////////////////////////////////////////////////// #include +#include + #include #include @@ -58,6 +60,12 @@ public: //////////////////////////////////////////////////////////// [[nodiscard]] static bool check(InputStream& stream); + //////////////////////////////////////////////////////////// + /// \brief Destructor + /// + //////////////////////////////////////////////////////////// + ~SoundFileReaderWav() override; + //////////////////////////////////////////////////////////// /// \brief Open a sound file for reading /// @@ -66,7 +74,7 @@ public: /// \return Properties of the loaded sound if the file was successfully opened /// //////////////////////////////////////////////////////////// - [[nodiscard]] std::optional open(sf::InputStream& stream) override; + [[nodiscard]] std::optional open(InputStream& stream) override; //////////////////////////////////////////////////////////// /// \brief Change the current read position to the given sample offset @@ -95,23 +103,11 @@ public: [[nodiscard]] std::uint64_t read(std::int16_t* samples, std::uint64_t maxCount) override; private: - //////////////////////////////////////////////////////////// - /// \brief Read the header of the open file - /// - /// \param info Attributes of the sound file - /// - /// \return True on success, false on error - /// - //////////////////////////////////////////////////////////// - std::optional parseHeader(); - //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - InputStream* m_stream{}; //!< Source stream to read from - unsigned int m_bytesPerSample{}; //!< Size of a sample, in bytes - std::uint64_t m_dataStart{}; //!< Starting position of the audio data in the open file - std::uint64_t m_dataEnd{}; //!< Position one byte past the end of the audio data in the open file + std::optional m_decoder; //!< wav decoder + ma_uint32 m_channelCount{}; //!< Number of channels }; } // namespace sf::priv diff --git a/src/SFML/Audio/SoundFileWriterOgg.hpp b/src/SFML/Audio/SoundFileWriterOgg.hpp index 50cf05b4c..7e9ce98e3 100644 --- a/src/SFML/Audio/SoundFileWriterOgg.hpp +++ b/src/SFML/Audio/SoundFileWriterOgg.hpp @@ -31,6 +31,7 @@ #include +#include #include #include @@ -103,12 +104,12 @@ private: //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - unsigned int m_channelCount{}; //!< Channel count of the sound being written - std::size_t m_remapTable[8]{}; //!< Table we use to remap source to target channel order - std::ofstream m_file; //!< Output file - ogg_stream_state m_ogg{}; //!< OGG stream - vorbis_info m_vorbis{}; //!< Vorbis handle - vorbis_dsp_state m_state{}; //!< Current encoding state + unsigned int m_channelCount{}; //!< Channel count of the sound being written + std::array m_remapTable{}; //!< Table we use to remap source to target channel order + std::ofstream m_file; //!< Output file + ogg_stream_state m_ogg{}; //!< OGG stream + vorbis_info m_vorbis{}; //!< Vorbis handle + vorbis_dsp_state m_state{}; //!< Current encoding state }; } // namespace sf::priv diff --git a/src/SFML/Audio/SoundFileWriterWav.cpp b/src/SFML/Audio/SoundFileWriterWav.cpp index 9af6e43fe..d7fdaf8fd 100644 --- a/src/SFML/Audio/SoundFileWriterWav.cpp +++ b/src/SFML/Audio/SoundFileWriterWav.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -45,25 +46,25 @@ namespace void encode(std::ostream& stream, std::int16_t value) { - const std::byte bytes[] = {static_cast(value & 0xFF), static_cast(value >> 8)}; - stream.write(reinterpret_cast(bytes), sizeof(bytes)); + const std::array bytes = {static_cast(value & 0xFF), static_cast(value >> 8)}; + stream.write(bytes.data(), bytes.size()); } void encode(std::ostream& stream, std::uint16_t value) { - const std::byte bytes[] = {static_cast(value & 0xFF), static_cast(value >> 8)}; - stream.write(reinterpret_cast(bytes), sizeof(bytes)); + const std::array bytes = {static_cast(value & 0xFF), static_cast(value >> 8)}; + stream.write(bytes.data(), bytes.size()); } void encode(std::ostream& stream, std::uint32_t value) { - const std::byte bytes[] = { - static_cast(value & 0x000000FF), - static_cast((value & 0x0000FF00) >> 8), - static_cast((value & 0x00FF0000) >> 16), - static_cast((value & 0xFF000000) >> 24), + const std::array bytes = { + static_cast(value & 0x000000FF), + static_cast((value & 0x0000FF00) >> 8), + static_cast((value & 0x00FF0000) >> 16), + static_cast((value & 0xFF000000) >> 24), }; - stream.write(reinterpret_cast(bytes), sizeof(bytes)); + stream.write(bytes.data(), bytes.size()); } } // namespace @@ -247,17 +248,17 @@ void SoundFileWriterWav::writeHeader(unsigned int sampleRate, unsigned int chann assert(m_file.good() && "Most recent I/O operation failed"); // Write the main chunk ID - char mainChunkId[4] = {'R', 'I', 'F', 'F'}; - m_file.write(mainChunkId, sizeof(mainChunkId)); + std::array mainChunkId = {'R', 'I', 'F', 'F'}; + m_file.write(mainChunkId.data(), mainChunkId.size()); // Write the main chunk header encode(m_file, static_cast(0)); // 0 is a placeholder, will be written later - char mainChunkFormat[4] = {'W', 'A', 'V', 'E'}; - m_file.write(mainChunkFormat, sizeof(mainChunkFormat)); + std::array mainChunkFormat = {'W', 'A', 'V', 'E'}; + m_file.write(mainChunkFormat.data(), mainChunkFormat.size()); // Write the sub-chunk 1 ("format") id and size - char fmtChunkId[4] = {'f', 'm', 't', ' '}; - m_file.write(fmtChunkId, sizeof(fmtChunkId)); + std::array fmtChunkId = {'f', 'm', 't', ' '}; + m_file.write(fmtChunkId.data(), fmtChunkId.size()); if (channelCount > 2) { @@ -295,14 +296,14 @@ void SoundFileWriterWav::writeHeader(unsigned int sampleRate, unsigned int chann encode(m_file, bitsPerSample); encode(m_file, channelMask); // Write the subformat (PCM) - char subformat[16] = + std::array subformat = {'\x01', '\x00', '\x00', '\x00', '\x00', '\x00', '\x10', '\x00', '\x80', '\x00', '\x00', '\xAA', '\x00', '\x38', '\x9B', '\x71'}; - m_file.write(subformat, sizeof(subformat)); + m_file.write(subformat.data(), subformat.size()); } // Write the sub-chunk 2 ("data") id and size - char dataChunkId[4] = {'d', 'a', 't', 'a'}; - m_file.write(dataChunkId, sizeof(dataChunkId)); + std::array dataChunkId = {'d', 'a', 't', 'a'}; + m_file.write(dataChunkId.data(), dataChunkId.size()); const std::uint32_t dataChunkSize = 0; // placeholder, will be written later encode(m_file, dataChunkSize); } diff --git a/src/SFML/Audio/SoundFileWriterWav.hpp b/src/SFML/Audio/SoundFileWriterWav.hpp index 4faf8f652..75f7c95ab 100644 --- a/src/SFML/Audio/SoundFileWriterWav.hpp +++ b/src/SFML/Audio/SoundFileWriterWav.hpp @@ -29,6 +29,7 @@ //////////////////////////////////////////////////////////// #include +#include #include #include @@ -105,9 +106,9 @@ private: //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - std::ofstream m_file; //!< File stream to write to - unsigned int m_channelCount{}; //!< Channel count of the sound being written - std::size_t m_remapTable[18]{}; //!< Table we use to remap source to target channel order + std::ofstream m_file; //!< File stream to write to + unsigned int m_channelCount{}; //!< Channel count of the sound being written + std::array m_remapTable{}; //!< Table we use to remap source to target channel order }; } // namespace sf::priv diff --git a/src/SFML/Audio/SoundRecorder.cpp b/src/SFML/Audio/SoundRecorder.cpp index 202ae0b04..da4493dce 100644 --- a/src/SFML/Audio/SoundRecorder.cpp +++ b/src/SFML/Audio/SoundRecorder.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -187,16 +188,52 @@ SoundRecorder::SoundRecorder() : m_impl(std::make_unique(this)) // Create the context m_impl->context.emplace(); - auto contextConfig = ma_context_config_init(); - contextConfig.pLog = &*m_impl->log; + auto contextConfig = ma_context_config_init(); + contextConfig.pLog = &*m_impl->log; + ma_uint32 deviceCount = 0; + const auto nullBackend = ma_backend_null; + const std::array backendLists{nullptr, &nullBackend}; - if (const auto result = ma_context_init(nullptr, 0, &contextConfig, &*m_impl->context); result != MA_SUCCESS) + for (const auto* backendList : backendLists) + { + // We can set backendCount to 1 since it is ignored when backends is set to nullptr + if (const auto result = ma_context_init(backendList, 1, &contextConfig, &*m_impl->context); result != MA_SUCCESS) + { + m_impl->context.reset(); + err() << "Failed to initialize the audio capture context: " << ma_result_description(result) << std::endl; + return; + } + + // Count the capture devices + if (const auto result = ma_context_get_devices(&*m_impl->context, nullptr, nullptr, nullptr, &deviceCount); + result != MA_SUCCESS) + { + err() << "Failed to get audio capture devices: " << ma_result_description(result) << std::endl; + return; + } + + // Check if there are audio capture devices available on the system + if (deviceCount > 0) + break; + + // Warn if no devices were found using the default backend list + if (backendList == nullptr) + err() << "No audio capture devices available on the system" << std::endl; + + // Clean up the context if we didn't find any devices + ma_context_uninit(&*m_impl->context); + } + + // If the NULL audio backend also doesn't provide a device we give up + if (deviceCount == 0) { m_impl->context.reset(); - err() << "Failed to initialize the audio context: " << ma_result_description(result) << std::endl; return; } + if (m_impl->context->backend == ma_backend_null) + err() << "Using NULL audio backend for capture" << std::endl; + // Create the capture device m_impl->initialize(); } diff --git a/src/SFML/Audio/SoundSource.cpp b/src/SFML/Audio/SoundSource.cpp index 7b7d67477..3bb156232 100644 --- a/src/SFML/Audio/SoundSource.cpp +++ b/src/SFML/Audio/SoundSource.cpp @@ -88,8 +88,8 @@ void SoundSource::setCone(const Cone& cone) { if (auto* sound = static_cast(getSound())) ma_sound_set_cone(sound, - std::clamp(cone.innerAngle, sf::degrees(0), sf::degrees(360)).asRadians(), - std::clamp(cone.outerAngle, sf::degrees(0), sf::degrees(360)).asRadians(), + std::clamp(cone.innerAngle, Angle::Zero, degrees(360.f)).asRadians(), + std::clamp(cone.outerAngle, Angle::Zero, degrees(360.f)).asRadians(), cone.outerGain); } @@ -166,6 +166,13 @@ void SoundSource::setAttenuation(float attenuation) } +//////////////////////////////////////////////////////////// +// NOLINTNEXTLINE(performance-unnecessary-value-param) +void SoundSource::setEffectProcessor(EffectProcessor) +{ +} + + //////////////////////////////////////////////////////////// float SoundSource::getPitch() const { @@ -241,12 +248,12 @@ SoundSource::Cone SoundSource::getCone() const float outerAngle = 0.f; Cone cone; ma_sound_get_cone(sound, &innerAngle, &outerAngle, &cone.outerGain); - cone.innerAngle = sf::radians(innerAngle); - cone.outerAngle = sf::radians(outerAngle); + cone.innerAngle = radians(innerAngle); + cone.outerAngle = radians(outerAngle); return cone; } - return Cone{sf::radians(0), sf::radians(0), 0.f}; + return Cone{radians(0), radians(0), 0.f}; } diff --git a/src/SFML/Audio/SoundStream.cpp b/src/SFML/Audio/SoundStream.cpp index 8299bae13..5ec0bbad6 100644 --- a/src/SFML/Audio/SoundStream.cpp +++ b/src/SFML/Audio/SoundStream.cpp @@ -55,6 +55,7 @@ struct SoundStream::Impl ~Impl() { ma_sound_uninit(&sound); + ma_node_uninit(&effectNode, nullptr); ma_data_source_uninit(&dataSourceBase); } @@ -91,6 +92,34 @@ struct SoundStream::Impl return; } + // Initialize the custom effect node + effectNodeVTable.onProcess = + [](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut) + { static_cast(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); }; + effectNodeVTable.onGetRequiredInputFrameCount = nullptr; + effectNodeVTable.inputBusCount = 1; + effectNodeVTable.outputBusCount = 1; + effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT; + + const auto nodeChannelCount = ma_engine_get_channels(engine); + ma_node_config nodeConfig = ma_node_config_init(); + nodeConfig.vtable = &effectNodeVTable; + nodeConfig.pInputChannels = &nodeChannelCount; + nodeConfig.pOutputChannels = &nodeChannelCount; + + if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode); + result != MA_SUCCESS) + { + err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl; + return; + } + + effectNode.impl = this; + effectNode.channelCount = nodeChannelCount; + + // Route the sound through the effect node depending on whether an effect processor is set + connectEffect(bool{effectProcessor}); + // Because we are providing a custom data source, we have to provide the channel map ourselves if (!channelMap.empty()) { @@ -111,7 +140,79 @@ struct SoundStream::Impl void reinitialize() { - priv::MiniaudioUtils::reinitializeSound(sound, [this] { initialize(); }); + priv::MiniaudioUtils::reinitializeSound(sound, + [this] + { + ma_node_uninit(&effectNode, nullptr); + initialize(); + }); + } + + void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const + { + // If a processor is set, call it + if (effectProcessor) + { + if (!framesIn) + frameCountIn = 0; + + effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount); + return; + } + + // Otherwise just pass the data through 1:1 + if (framesIn == nullptr) + { + frameCountIn = 0; + frameCountOut = 0; + return; + } + + const auto toProcess = std::min(frameCountIn, frameCountOut); + std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float)); + frameCountIn = toProcess; + frameCountOut = toProcess; + } + + void connectEffect(bool connect) + { + auto* engine = priv::AudioDevice::get().getEngine(); + + if (engine == nullptr) + { + err() << "Failed to connect effect: No engine available" << std::endl; + return; + } + + if (connect) + { + // Attach the custom effect node output to our engine endpoint + if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0); + result != MA_SUCCESS) + { + err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl; + return; + } + } + else + { + // Detach the custom effect node output from our engine endpoint + if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS) + { + err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result) + << std::endl; + return; + } + } + + // Attach the sound output to the custom effect node or the engine endpoint + if (const ma_result + result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0); + result != MA_SUCCESS) + { + err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl; + return; + } } static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) @@ -238,8 +339,17 @@ struct SoundStream::Impl //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// + struct EffectNode + { + ma_node_base base{}; + Impl* impl{}; + ma_uint32 channelCount{}; + }; + ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) SoundStream* const owner; //!< Owning SoundStream object + ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node + EffectNode effectNode; //!< The engine node that performs effect processing std::vector soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) ma_sound sound{}; //!< The sound std::vector sampleBuffer; //!< Our temporary sample buffer @@ -251,6 +361,7 @@ struct SoundStream::Impl bool loop{}; //!< Loop flag (true to loop, false to play once) bool streaming{true}; //!< True if we are still streaming samples from the source Status status{Status::Stopped}; //!< The status + EffectProcessor effectProcessor; //!< The effect processor }; @@ -395,6 +506,14 @@ bool SoundStream::getLoop() const } +//////////////////////////////////////////////////////////// +void SoundStream::setEffectProcessor(EffectProcessor effectProcessor) +{ + m_impl->effectProcessor = std::move(effectProcessor); + m_impl->connectEffect(bool{m_impl->effectProcessor}); +} + + //////////////////////////////////////////////////////////// std::optional SoundStream::onLoop() { diff --git a/src/SFML/Graphics/CircleShape.cpp b/src/SFML/Graphics/CircleShape.cpp index f2b5901f3..7edb3eadb 100644 --- a/src/SFML/Graphics/CircleShape.cpp +++ b/src/SFML/Graphics/CircleShape.cpp @@ -71,7 +71,7 @@ std::size_t CircleShape::getPointCount() const //////////////////////////////////////////////////////////// Vector2f CircleShape::getPoint(std::size_t index) const { - const Angle angle = static_cast(index) / static_cast(m_pointCount) * sf::degrees(360) - sf::degrees(90); + const Angle angle = static_cast(index) / static_cast(m_pointCount) * degrees(360.f) - degrees(90.f); return Vector2f(m_radius, m_radius) + Vector2f(m_radius, angle); } diff --git a/src/SFML/Graphics/Font.cpp b/src/SFML/Graphics/Font.cpp index b009d65e4..97a566330 100644 --- a/src/SFML/Graphics/Font.cpp +++ b/src/SFML/Graphics/Font.cpp @@ -784,7 +784,7 @@ bool Font::setCurrentSize(unsigned int characterSize) const Font::Page::Page(bool smooth) { // Make sure that the texture is initialized by default - sf::Image image; + Image image; image.create({128, 128}, Color::Transparent); // Reserve a 2x2 white square for texturing underlines diff --git a/src/SFML/Graphics/GLExtensions.cpp b/src/SFML/Graphics/GLExtensions.cpp index ce8bb4eb4..310644308 100644 --- a/src/SFML/Graphics/GLExtensions.cpp +++ b/src/SFML/Graphics/GLExtensions.cpp @@ -61,9 +61,9 @@ void ensureExtensionsInit() initialized = true; #ifdef SFML_OPENGL_ES - gladLoadGLES1(sf::Context::getFunction); + gladLoadGLES1(Context::getFunction); #else - gladLoadGL(sf::Context::getFunction); + gladLoadGL(Context::getFunction); #endif // Retrieve the context version number diff --git a/src/SFML/Graphics/Image.cpp b/src/SFML/Graphics/Image.cpp index ded88ea1a..62d9fe940 100644 --- a/src/SFML/Graphics/Image.cpp +++ b/src/SFML/Graphics/Image.cpp @@ -31,6 +31,7 @@ #include #include #ifdef SFML_SYSTEM_ANDROID +#include #include #endif @@ -157,7 +158,15 @@ void Image::create(const Vector2u& size, const std::uint8_t* pixels) //////////////////////////////////////////////////////////// bool Image::loadFromFile(const std::filesystem::path& filename) { -#ifndef SFML_SYSTEM_ANDROID +#ifdef SFML_SYSTEM_ANDROID + + if (priv::getActivityStatesPtr() != nullptr) + { + priv::ResourceStream stream(filename); + return loadFromStream(stream); + } + +#endif // Clear the array (just in case) m_pixels.clear(); @@ -186,13 +195,6 @@ bool Image::loadFromFile(const std::filesystem::path& filename) return false; } - -#else - - priv::ResourceStream stream(filename); - return loadFromStream(stream); - -#endif } diff --git a/src/SFML/Graphics/Shader.cpp b/src/SFML/Graphics/Shader.cpp index f1a97e2db..f7d669ae7 100644 --- a/src/SFML/Graphics/Shader.cpp +++ b/src/SFML/Graphics/Shader.cpp @@ -740,7 +740,7 @@ bool Shader::isAvailable() const TransientContextLock contextLock; // Make sure that extensions are initialized - sf::priv::ensureExtensionsInit(); + priv::ensureExtensionsInit(); return GLEXT_multitexture && GLEXT_shading_language_100 && GLEXT_shader_objects && GLEXT_vertex_shader && GLEXT_fragment_shader; @@ -758,7 +758,7 @@ bool Shader::isGeometryAvailable() const TransientContextLock contextLock; // Make sure that extensions are initialized - sf::priv::ensureExtensionsInit(); + priv::ensureExtensionsInit(); return isAvailable() && (GLEXT_geometry_shader4 || GLEXT_GL_VERSION_3_2); }(); diff --git a/src/SFML/Graphics/Text.cpp b/src/SFML/Graphics/Text.cpp index 33ca33c88..518e535d4 100644 --- a/src/SFML/Graphics/Text.cpp +++ b/src/SFML/Graphics/Text.cpp @@ -381,7 +381,7 @@ void Text::ensureGeometryUpdate() const const bool isBold = m_style & Bold; const bool isUnderlined = m_style & Underlined; const bool isStrikeThrough = m_style & StrikeThrough; - const float italicShear = (m_style & Italic) ? sf::degrees(12).asRadians() : 0.f; + const float italicShear = (m_style & Italic) ? degrees(12).asRadians() : 0.f; const float underlineOffset = m_font->getUnderlinePosition(m_characterSize); const float underlineThickness = m_font->getUnderlineThickness(m_characterSize); diff --git a/src/SFML/Graphics/Texture.cpp b/src/SFML/Graphics/Texture.cpp index a66699b9b..dd933de10 100644 --- a/src/SFML/Graphics/Texture.cpp +++ b/src/SFML/Graphics/Texture.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -861,10 +862,10 @@ void Texture::bind(const Texture* texture, CoordinateType coordinateType) if ((coordinateType == CoordinateType::Pixels) || texture->m_pixelsFlipped) { // clang-format off - GLfloat matrix[16] = {1.f, 0.f, 0.f, 0.f, - 0.f, 1.f, 0.f, 0.f, - 0.f, 0.f, 1.f, 0.f, - 0.f, 0.f, 0.f, 1.f}; + std::array matrix = {1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f}; // clang-format on // If non-normalized coordinates (= pixels) are requested, we need to @@ -884,7 +885,7 @@ void Texture::bind(const Texture* texture, CoordinateType coordinateType) // Load the matrix glCheck(glMatrixMode(GL_TEXTURE)); - glCheck(glLoadMatrixf(matrix)); + glCheck(glLoadMatrixf(matrix.data())); } else { @@ -921,7 +922,7 @@ unsigned int Texture::getMaximumSize() GLint value = 0; // Make sure that extensions are initialized - sf::priv::ensureExtensionsInit(); + priv::ensureExtensionsInit(); glCheck(glGetIntegerv(GL_MAX_TEXTURE_SIZE, &value)); diff --git a/src/SFML/Network/Packet.cpp b/src/SFML/Network/Packet.cpp index 498c14677..e639c049a 100644 --- a/src/SFML/Network/Packet.cpp +++ b/src/SFML/Network/Packet.cpp @@ -31,6 +31,8 @@ #include #include +#include + #include #include @@ -212,8 +214,8 @@ Packet& Packet::operator>>(std::uint64_t& data) { // Since ntohll is not available everywhere, we have to convert // to network byte order (big endian) manually - std::byte bytes[sizeof(data)]; - std::memcpy(bytes, &m_data[m_readPos], sizeof(data)); + std::array bytes{}; + std::memcpy(bytes.data(), &m_data[m_readPos], sizeof(data)); data = toInteger(bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]); @@ -427,14 +429,14 @@ Packet& Packet::operator<<(std::int64_t data) // Since htonll is not available everywhere, we have to convert // to network byte order (big endian) manually - std::uint8_t toWrite[] = {static_cast((data >> 56) & 0xFF), - static_cast((data >> 48) & 0xFF), - static_cast((data >> 40) & 0xFF), - static_cast((data >> 32) & 0xFF), - static_cast((data >> 24) & 0xFF), - static_cast((data >> 16) & 0xFF), - static_cast((data >> 8) & 0xFF), - static_cast((data)&0xFF)}; + std::array toWrite = {static_cast((data >> 56) & 0xFF), + static_cast((data >> 48) & 0xFF), + static_cast((data >> 40) & 0xFF), + static_cast((data >> 32) & 0xFF), + static_cast((data >> 24) & 0xFF), + static_cast((data >> 16) & 0xFF), + static_cast((data >> 8) & 0xFF), + static_cast((data)&0xFF)}; append(&toWrite, sizeof(toWrite)); return *this; @@ -447,14 +449,14 @@ Packet& Packet::operator<<(std::uint64_t data) // Since htonll is not available everywhere, we have to convert // to network byte order (big endian) manually - std::uint8_t toWrite[] = {static_cast((data >> 56) & 0xFF), - static_cast((data >> 48) & 0xFF), - static_cast((data >> 40) & 0xFF), - static_cast((data >> 32) & 0xFF), - static_cast((data >> 24) & 0xFF), - static_cast((data >> 16) & 0xFF), - static_cast((data >> 8) & 0xFF), - static_cast((data)&0xFF)}; + std::array toWrite = {static_cast((data >> 56) & 0xFF), + static_cast((data >> 48) & 0xFF), + static_cast((data >> 40) & 0xFF), + static_cast((data >> 32) & 0xFF), + static_cast((data >> 24) & 0xFF), + static_cast((data >> 16) & 0xFF), + static_cast((data >> 8) & 0xFF), + static_cast((data)&0xFF)}; append(&toWrite, sizeof(toWrite)); return *this; diff --git a/src/SFML/Network/TcpSocket.cpp b/src/SFML/Network/TcpSocket.cpp index 31a6249e0..fb24f2f16 100644 --- a/src/SFML/Network/TcpSocket.cpp +++ b/src/SFML/Network/TcpSocket.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -401,12 +402,12 @@ Socket::Status TcpSocket::receive(Packet& packet) } // Loop until we receive all the packet data - char buffer[1024]; + std::array buffer{}; while (m_pendingPacket.data.size() < packetSize) { // Receive a chunk of data const std::size_t sizeToGet = std::min(packetSize - m_pendingPacket.data.size(), sizeof(buffer)); - const Status status = receive(buffer, sizeToGet, received); + const Status status = receive(buffer.data(), sizeToGet, received); if (status != Status::Done) return status; @@ -415,7 +416,7 @@ Socket::Status TcpSocket::receive(Packet& packet) { m_pendingPacket.data.resize(m_pendingPacket.data.size() + received); std::byte* begin = m_pendingPacket.data.data() + m_pendingPacket.data.size() - received; - std::memcpy(begin, buffer, received); + std::memcpy(begin, buffer.data(), received); } } diff --git a/src/SFML/PCH.hpp b/src/SFML/PCH.hpp index 092a779ba..e75ee2ca8 100644 --- a/src/SFML/PCH.hpp +++ b/src/SFML/PCH.hpp @@ -45,6 +45,7 @@ #include #include +#include #include #include #include diff --git a/src/SFML/System/FileInputStream.cpp b/src/SFML/System/FileInputStream.cpp index 38d8467b8..b523ff797 100644 --- a/src/SFML/System/FileInputStream.cpp +++ b/src/SFML/System/FileInputStream.cpp @@ -27,6 +27,7 @@ //////////////////////////////////////////////////////////// #include #ifdef SFML_SYSTEM_ANDROID +#include #include #endif #include @@ -36,12 +37,10 @@ namespace sf { //////////////////////////////////////////////////////////// -#ifndef SFML_SYSTEM_ANDROID void FileInputStream::FileCloser::operator()(std::FILE* file) { std::fclose(file); } -#endif //////////////////////////////////////////////////////////// @@ -64,69 +63,88 @@ FileInputStream& FileInputStream::operator=(FileInputStream&&) noexcept = defaul bool FileInputStream::open(const std::filesystem::path& filename) { #ifdef SFML_SYSTEM_ANDROID - m_file = std::make_unique(filename); - return m_file->tell() != -1; -#else + if (priv::getActivityStatesPtr() != nullptr) + { + m_androidFile = std::make_unique(filename); + return m_androidFile->tell() != -1; + } +#endif #ifdef SFML_SYSTEM_WINDOWS m_file.reset(_wfopen(filename.c_str(), L"rb")); #else m_file.reset(std::fopen(filename.c_str(), "rb")); #endif return m_file != nullptr; -#endif } //////////////////////////////////////////////////////////// std::int64_t FileInputStream::read(void* data, std::int64_t size) { +#ifdef SFML_SYSTEM_ANDROID + if (priv::getActivityStatesPtr() != nullptr) + { + if (!m_androidFile) + return -1; + return m_androidFile->read(data, size); + } +#endif if (!m_file) return -1; -#ifdef SFML_SYSTEM_ANDROID - return m_file->read(data, size); -#else return static_cast(std::fread(data, 1, static_cast(size), m_file.get())); -#endif } //////////////////////////////////////////////////////////// std::int64_t FileInputStream::seek(std::int64_t position) { +#ifdef SFML_SYSTEM_ANDROID + if (priv::getActivityStatesPtr() != nullptr) + { + if (!m_androidFile) + return -1; + return m_androidFile->seek(position); + } +#endif if (!m_file) return -1; -#ifdef SFML_SYSTEM_ANDROID - return m_file->seek(position); -#else if (std::fseek(m_file.get(), static_cast(position), SEEK_SET)) return -1; return tell(); -#endif } //////////////////////////////////////////////////////////// std::int64_t FileInputStream::tell() { +#ifdef SFML_SYSTEM_ANDROID + if (priv::getActivityStatesPtr() != nullptr) + { + if (!m_androidFile) + return -1; + return m_androidFile->tell(); + } +#endif if (!m_file) return -1; -#ifdef SFML_SYSTEM_ANDROID - return m_file->tell(); -#else return std::ftell(m_file.get()); -#endif } //////////////////////////////////////////////////////////// std::int64_t FileInputStream::getSize() { +#ifdef SFML_SYSTEM_ANDROID + if (priv::getActivityStatesPtr() != nullptr) + { + if (!m_androidFile) + return -1; + return m_androidFile->getSize(); + } +#endif if (!m_file) return -1; -#ifdef SFML_SYSTEM_ANDROID - return m_file->getSize(); -#else const std::int64_t position = tell(); std::fseek(m_file.get(), 0, SEEK_END); const std::int64_t size = tell(); @@ -135,7 +153,6 @@ std::int64_t FileInputStream::getSize() return -1; return size; -#endif } } // namespace sf diff --git a/src/SFML/System/String.cpp b/src/SFML/System/String.cpp index 99b381fce..36c9fb250 100644 --- a/src/SFML/System/String.cpp +++ b/src/SFML/System/String.cpp @@ -264,10 +264,10 @@ std::wstring String::toWideString() const //////////////////////////////////////////////////////////// -sf::U8String String::toUtf8() const +U8String String::toUtf8() const { // Prepare the output string - sf::U8String output; + U8String output; output.reserve(m_string.length()); // Convert diff --git a/src/SFML/Window/Android/ClipboardImpl.cpp b/src/SFML/Window/Android/ClipboardImpl.cpp index a6a55964f..8acff3268 100644 --- a/src/SFML/Window/Android/ClipboardImpl.cpp +++ b/src/SFML/Window/Android/ClipboardImpl.cpp @@ -38,7 +38,7 @@ namespace sf::priv //////////////////////////////////////////////////////////// String ClipboardImpl::getString() { - sf::err() << "Clipboard API not implemented for Android.\n"; + err() << "Clipboard API not implemented for Android.\n"; return {}; } @@ -46,7 +46,7 @@ String ClipboardImpl::getString() //////////////////////////////////////////////////////////// void ClipboardImpl::setString(const String& /* text */) { - sf::err() << "Clipboard API not implemented for Android.\n"; + err() << "Clipboard API not implemented for Android.\n"; } } // namespace sf::priv diff --git a/src/SFML/Window/JoystickImpl.hpp b/src/SFML/Window/JoystickImpl.hpp index 7ef509bc9..350a3fdf9 100644 --- a/src/SFML/Window/JoystickImpl.hpp +++ b/src/SFML/Window/JoystickImpl.hpp @@ -55,7 +55,7 @@ struct JoystickState { bool connected{}; //!< Is the joystick currently connected? EnumArray axes{}; //!< Position of each axis, in range [-100, 100] - bool buttons[Joystick::ButtonCount]{}; //!< Status of each button (true = pressed) + std::array buttons{}; //!< Status of each button (true = pressed) }; } // namespace sf::priv diff --git a/src/SFML/Window/JoystickManager.hpp b/src/SFML/Window/JoystickManager.hpp index a26ceaf5e..585b3ddf4 100644 --- a/src/SFML/Window/JoystickManager.hpp +++ b/src/SFML/Window/JoystickManager.hpp @@ -30,6 +30,8 @@ #include #include +#include + namespace sf::priv { @@ -124,7 +126,7 @@ private: //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - Item m_joysticks[Joystick::Count]; //!< Joysticks information and state + std::array m_joysticks; //!< Joysticks information and state }; } // namespace sf::priv diff --git a/src/SFML/Window/Unix/GlxContext.cpp b/src/SFML/Window/Unix/GlxContext.cpp index 842d43338..1f1629f8d 100644 --- a/src/SFML/Window/Unix/GlxContext.cpp +++ b/src/SFML/Window/Unix/GlxContext.cpp @@ -33,6 +33,7 @@ #include +#include #include #include #include @@ -513,10 +514,10 @@ void GlxContext::createSurface(GlxContext* shared, const Vector2u& size, unsigne if (config) { - int attributes[] = + std::array attributes = {GLX_PBUFFER_WIDTH, static_cast(size.x), GLX_PBUFFER_HEIGHT, static_cast(size.y), 0, 0}; - m_pbuffer = glXCreatePbuffer(m_display.get(), *config, attributes); + m_pbuffer = glXCreatePbuffer(m_display.get(), *config, attributes.data()); updateSettingsFromVisualInfo(&visualInfo); @@ -578,11 +579,11 @@ void GlxContext::createContext(GlxContext* shared) glXQueryDrawable(m_display.get(), m_pbuffer, GLX_FBCONFIG_ID, &fbConfigId); - int attributes[] = {GLX_FBCONFIG_ID, static_cast(fbConfigId), 0, 0}; + std::array attributes = {GLX_FBCONFIG_ID, static_cast(fbConfigId), 0, 0}; int count = 0; const auto fbconfig = X11Ptr( - glXChooseFBConfig(m_display.get(), DefaultScreen(m_display.get()), attributes, &count)); + glXChooseFBConfig(m_display.get(), DefaultScreen(m_display.get()), attributes.data(), &count)); if (count == 1) visualInfo = X11Ptr(glXGetVisualFromFBConfig(m_display.get(), *fbconfig)); diff --git a/src/SFML/Window/Unix/JoystickImpl.cpp b/src/SFML/Window/Unix/JoystickImpl.cpp index 308037993..316898cf6 100644 --- a/src/SFML/Window/Unix/JoystickImpl.cpp +++ b/src/SFML/Window/Unix/JoystickImpl.cpp @@ -400,14 +400,13 @@ std::string getJoystickName(unsigned int index) if (fd >= 0) { // Get the name - char name[128] = {}; - - const int result = ioctl(fd, JSIOCGNAME(sizeof(name)), name); + std::array name{}; + const int result = ioctl(fd, JSIOCGNAME(name.size()), name.data()); ::close(fd); if (result >= 0) - return name; + return name.data(); } // Fall back to manual USB chain walk via udev @@ -600,7 +599,7 @@ JoystickCaps JoystickImpl::getCapabilities() const ioctl(m_file, JSIOCGAXES, &axesCount); for (int i = 0; i < axesCount; ++i) { - switch (m_mapping[i]) + switch (m_mapping[static_cast(i)]) { // clang-format off case ABS_X: caps.axes[Joystick::Axis::X] = true; break; @@ -650,7 +649,7 @@ JoystickState JoystickImpl::JoystickImpl::update() { const float value = joyState.value * 100.f / 32767.f; - if (joyState.number < ABS_MAX + 1) + if (joyState.number < m_mapping.size()) { switch (m_mapping[joyState.number]) { diff --git a/src/SFML/Window/Unix/JoystickImpl.hpp b/src/SFML/Window/Unix/JoystickImpl.hpp index 1d16750de..843b983c5 100644 --- a/src/SFML/Window/Unix/JoystickImpl.hpp +++ b/src/SFML/Window/Unix/JoystickImpl.hpp @@ -105,10 +105,10 @@ private: //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - int m_file{-1}; ///< File descriptor of the joystick - char m_mapping[ABS_MAX + 1]{0}; ///< Axes mapping (index to axis id) - JoystickState m_state; ///< Current state of the joystick - sf::Joystick::Identification m_identification; ///< Identification of the joystick + int m_file{-1}; ///< File descriptor of the joystick + std::array m_mapping{}; ///< Axes mapping (index to axis id) + JoystickState m_state; ///< Current state of the joystick + Joystick::Identification m_identification; ///< Identification of the joystick }; } // namespace sf::priv diff --git a/src/SFML/Window/WindowBase.cpp b/src/SFML/Window/WindowBase.cpp index f437be7fc..aa72f8e93 100644 --- a/src/SFML/Window/WindowBase.cpp +++ b/src/SFML/Window/WindowBase.cpp @@ -138,7 +138,7 @@ bool WindowBase::isOpen() const //////////////////////////////////////////////////////////// Event WindowBase::pollEvent() { - sf::Event event; + Event event; if (m_impl && (event = m_impl->popEvent(false))) filterEvent(event); return event; @@ -148,7 +148,7 @@ Event WindowBase::pollEvent() //////////////////////////////////////////////////////////// Event WindowBase::waitEvent() { - sf::Event event; + Event event; if (m_impl && (event = m_impl->popEvent(true))) filterEvent(event); return event; diff --git a/src/SFML/Window/WindowImpl.cpp b/src/SFML/Window/WindowImpl.cpp index 5825766ef..c903a7f87 100644 --- a/src/SFML/Window/WindowImpl.cpp +++ b/src/SFML/Window/WindowImpl.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include @@ -96,7 +97,7 @@ namespace sf::priv //////////////////////////////////////////////////////////// struct WindowImpl::JoystickStatesImpl { - JoystickState states[Joystick::Count]{}; //!< Previous state of the joysticks + std::array states{}; //!< Previous state of the joysticks }; @@ -131,7 +132,7 @@ WindowImpl::WindowImpl() : m_joystickStatesImpl(std::make_unique(manager.gyroData.rotationRate.x)); - value.y = toDegrees(static_cast(manager.gyroData.rotationRate.y)); - value.z = toDegrees(static_cast(manager.gyroData.rotationRate.z)); + // Rotation rates are given in rad/s + value.x = static_cast(manager.gyroData.rotationRate.x); + value.y = static_cast(manager.gyroData.rotationRate.y); + value.z = static_cast(manager.gyroData.rotationRate.z); break; case Sensor::Type::Magnetometer: @@ -166,10 +161,10 @@ Vector3f SensorImpl::update() break; case Sensor::Type::Orientation: - // Absolute rotation (Euler) angles are given in radians, convert to degrees - value.x = toDegrees(static_cast(manager.deviceMotion.attitude.yaw)); - value.y = toDegrees(static_cast(manager.deviceMotion.attitude.pitch)); - value.z = toDegrees(static_cast(manager.deviceMotion.attitude.roll)); + // Absolute rotation (Euler) angles are given in radians + value.x = static_cast(manager.deviceMotion.attitude.yaw); + value.y = static_cast(manager.deviceMotion.attitude.pitch); + value.z = static_cast(manager.deviceMotion.attitude.roll); break; default: diff --git a/src/SFML/Window/macOS/HIDJoystickManager.cpp b/src/SFML/Window/macOS/HIDJoystickManager.cpp index 0589bdd18..3c05db8cc 100644 --- a/src/SFML/Window/macOS/HIDJoystickManager.cpp +++ b/src/SFML/Window/macOS/HIDJoystickManager.cpp @@ -29,6 +29,8 @@ #include #include +#include + //////////////////////////////////////////////////////////// // Private data //////////////////////////////////////////////////////////// @@ -75,11 +77,8 @@ HIDJoystickManager::HIDJoystickManager() CFDictionaryRef mask1 = HIDInputManager::copyDevicesMask(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad); - CFDictionaryRef maskArray[2]; - maskArray[0] = mask0; - maskArray[1] = mask1; - - CFArrayRef mask = CFArrayCreate(nullptr, reinterpret_cast(maskArray), 2, nullptr); + std::array maskArray = {mask0, mask1}; + CFArrayRef mask = CFArrayCreate(nullptr, reinterpret_cast(maskArray.data()), maskArray.size(), nullptr); IOHIDManagerSetDeviceMatchingMultiple(m_manager, mask); CFRelease(mask); diff --git a/src/SFML/Window/macOS/JoystickImpl.hpp b/src/SFML/Window/macOS/JoystickImpl.hpp index d5cde0cf7..d998a1a10 100644 --- a/src/SFML/Window/macOS/JoystickImpl.hpp +++ b/src/SFML/Window/macOS/JoystickImpl.hpp @@ -111,7 +111,7 @@ private: // Member data //////////////////////////////////////////////////////////// using Location = long; - using AxisMap = std::unordered_map; + using AxisMap = std::unordered_map; using ButtonsVector = std::vector; AxisMap m_axis; ///< Axes (but not POV/Hat) of the joystick @@ -121,7 +121,7 @@ private: Joystick::Identification m_identification; ///< Joystick identification // NOLINTNEXTLINE(readability-identifier-naming) - static inline Location m_locationIDs[sf::Joystick::Count]{}; ///< Global Joystick register + static inline std::array m_locationIDs{}; ///< Global Joystick register /// For a corresponding SFML index, m_locationIDs is either some USB /// location or 0 if there isn't currently a connected joystick device }; diff --git a/src/SFML/Window/macOS/SFContext.mm b/src/SFML/Window/macOS/SFContext.mm index 7bffa1b02..f55eacfb5 100644 --- a/src/SFML/Window/macOS/SFContext.mm +++ b/src/SFML/Window/macOS/SFContext.mm @@ -229,7 +229,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons { if (!(m_settings.attributeFlags & ContextSettings::Core)) { - sf::err() << "Warning. Compatibility profile not supported on this platform." << std::endl; + err() << "Warning. Compatibility profile not supported on this platform." << std::endl; m_settings.attributeFlags |= ContextSettings::Core; } m_settings.majorVersion = 3; @@ -240,7 +240,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons if (m_settings.attributeFlags & ContextSettings::Debug) { - sf::err() << "Warning. OpenGL debugging not supported on this platform." << std::endl; + err() << "Warning. OpenGL debugging not supported on this platform." << std::endl; m_settings.attributeFlags &= ~static_cast(ContextSettings::Debug); } @@ -254,7 +254,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons if (pixFmt == nil) { - sf::err() << "Error. Unable to find a suitable pixel format." << std::endl; + err() << "Error. Unable to find a suitable pixel format." << std::endl; return; } @@ -267,7 +267,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons if (sharedContext == [NSOpenGLContext currentContext]) { - sf::err() << "Failed to deactivate shared context before sharing" << std::endl; + err() << "Failed to deactivate shared context before sharing" << std::endl; return; } } @@ -277,13 +277,13 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons if (m_context == nil) { - sf::err() << "Error. Unable to create the context. Retrying without shared context." << std::endl; + err() << "Error. Unable to create the context. Retrying without shared context." << std::endl; m_context = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:nil]; if (m_context == nil) - sf::err() << "Error. Unable to create the context." << std::endl; + err() << "Error. Unable to create the context." << std::endl; else - sf::err() << "Warning. New context created without shared context." << std::endl; + err() << "Warning. New context created without shared context." << std::endl; } // Free up. diff --git a/test/Audio/Music.test.cpp b/test/Audio/Music.test.cpp index 150771a1f..68bd03a4d 100644 --- a/test/Audio/Music.test.cpp +++ b/test/Audio/Music.test.cpp @@ -1,8 +1,209 @@ #include +// Other 1st party headers +#include + +#include + +#include +#include +#include +#include +#include #include -static_assert(!std::is_copy_constructible_v); -static_assert(!std::is_copy_assignable_v); -static_assert(!std::is_nothrow_move_constructible_v); -static_assert(!std::is_nothrow_move_assignable_v); +TEST_CASE("[Audio] sf::Music", runAudioDeviceTests()) +{ + SECTION("Type traits") + { + STATIC_CHECK(!std::is_copy_constructible_v); + STATIC_CHECK(!std::is_copy_assignable_v); + STATIC_CHECK(!std::is_nothrow_move_constructible_v); + STATIC_CHECK(!std::is_nothrow_move_assignable_v); + STATIC_CHECK(std::has_virtual_destructor_v); + } + + SECTION("Span") + { + const sf::Music::Span span; + CHECK(span.offset == 0); + CHECK(span.length == 0); + + const sf::Music::TimeSpan timeSpan; + CHECK(timeSpan.offset == sf::Time::Zero); + CHECK(timeSpan.length == sf::Time::Zero); + } + + SECTION("Construction") + { + const sf::Music music; + CHECK(music.getDuration() == sf::Time::Zero); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::Time::Zero); + CHECK(music.getChannelCount() == 0); + CHECK(music.getSampleRate() == 0); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + + SECTION("openFromFile()") + { + sf::Music music; + + SECTION("Invalid file") + { + REQUIRE(!music.openFromFile("does/not/exist.wav")); + CHECK(music.getDuration() == sf::Time::Zero); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::Time::Zero); + CHECK(music.getChannelCount() == 0); + CHECK(music.getSampleRate() == 0); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + + SECTION("Valid file") + { + REQUIRE(music.openFromFile("Audio/ding.mp3")); + CHECK(music.getDuration() == sf::microseconds(1990884)); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::microseconds(1990884)); + CHECK(music.getChannelCount() == 1); + CHECK(music.getSampleRate() == 44100); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + } + + SECTION("openFromMemory()") + { + std::vector memory; + sf::Music music; + + SECTION("Invalid buffer") + { + REQUIRE(!music.openFromMemory(memory.data(), memory.size())); + CHECK(music.getDuration() == sf::Time::Zero); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::Time::Zero); + CHECK(music.getChannelCount() == 0); + CHECK(music.getSampleRate() == 0); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + + SECTION("Valid buffer") + { + memory = loadIntoMemory("Audio/ding.flac"); + REQUIRE(music.openFromMemory(memory.data(), memory.size())); + CHECK(music.getDuration() == sf::microseconds(1990884)); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::microseconds(1990884)); + CHECK(music.getChannelCount() == 1); + CHECK(music.getSampleRate() == 44100); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + } + + SECTION("openFromStream()") + { + sf::FileInputStream stream; + sf::Music music; + + SECTION("Invalid stream") + { + CHECK(!music.openFromStream(stream)); + CHECK(music.getDuration() == sf::Time::Zero); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::Time::Zero); + CHECK(music.getChannelCount() == 0); + CHECK(music.getSampleRate() == 0); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + + SECTION("Valid stream") + { + REQUIRE(stream.open("Audio/doodle_pop.ogg")); + REQUIRE(music.openFromStream(stream)); + CHECK(music.getDuration() == sf::microseconds(24002176)); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::microseconds(24002176)); + CHECK(music.getChannelCount() == 2); + CHECK(music.getSampleRate() == 44100); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + } + + SECTION("play/pause/stop") + { + sf::Music music; + REQUIRE(music.openFromFile("Audio/ding.mp3")); + + // Wait for background thread to start + music.play(); + while (music.getStatus() == sf::Music::Status::Stopped) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + CHECK(music.getStatus() == sf::Music::Status::Playing); + + // Wait for background thread to pause + music.pause(); + while (music.getStatus() == sf::Music::Status::Playing) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + CHECK(music.getStatus() == sf::Music::Status::Paused); + + // Wait for background thread to stop + music.stop(); + while (music.getStatus() == sf::Music::Status::Paused) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + } + + SECTION("setLoopPoints()") + { + sf::Music music; + + SECTION("No file") + { + music.setLoopPoints({sf::Time::Zero, sf::Time::Zero}); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::Time::Zero); + CHECK(length == sf::Time::Zero); + CHECK(music.getChannelCount() == 0); + CHECK(music.getSampleRate() == 0); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + + SECTION("Loaded file") + { + REQUIRE(music.openFromFile("Audio/killdeer.wav")); + music.setLoopPoints({sf::seconds(1), sf::seconds(2)}); + const auto [offset, length] = music.getLoopPoints(); + CHECK(offset == sf::seconds(1)); + CHECK(length == sf::seconds(2)); + CHECK(music.getChannelCount() == 1); + CHECK(music.getSampleRate() == 22050); + CHECK(music.getStatus() == sf::Music::Status::Stopped); + CHECK(music.getPlayingOffset() == sf::Time::Zero); + CHECK(!music.getLoop()); + } + } +} diff --git a/test/Audio/Sound.test.cpp b/test/Audio/Sound.test.cpp index f0944d743..c650f1f16 100644 --- a/test/Audio/Sound.test.cpp +++ b/test/Audio/Sound.test.cpp @@ -1,11 +1,86 @@ #include +// Other 1st party headers +#include + +#include + +#include + +#include +#include #include -static_assert(!std::is_constructible_v); -static_assert(std::is_copy_constructible_v); -static_assert(std::is_copy_assignable_v); -static_assert(std::is_move_constructible_v); -static_assert(!std::is_nothrow_move_constructible_v); -static_assert(std::is_move_assignable_v); -static_assert(!std::is_nothrow_move_assignable_v); +TEST_CASE("[Audio] sf::Sound", runAudioDeviceTests()) +{ + SECTION("Type traits") + { + STATIC_CHECK(!std::is_constructible_v); + STATIC_CHECK(std::is_copy_constructible_v); + STATIC_CHECK(std::is_copy_assignable_v); + STATIC_CHECK(std::is_move_constructible_v); + STATIC_CHECK(!std::is_nothrow_move_constructible_v); + STATIC_CHECK(std::is_move_assignable_v); + STATIC_CHECK(!std::is_nothrow_move_assignable_v); + STATIC_CHECK(std::has_virtual_destructor_v); + } + + sf::SoundBuffer soundBuffer; + REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac")); + + SECTION("Construction") + { + const sf::Sound sound(soundBuffer); + CHECK(&sound.getBuffer() == &soundBuffer); + CHECK(!sound.getLoop()); + CHECK(sound.getPlayingOffset() == sf::Time::Zero); + CHECK(sound.getStatus() == sf::Sound::Status::Stopped); + } + + SECTION("Copy semantics") + { + const sf::Sound sound(soundBuffer); + + SECTION("Construction") + { + const sf::Sound soundCopy(sound); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(&soundCopy.getBuffer() == &soundBuffer); + CHECK(!soundCopy.getLoop()); + CHECK(soundCopy.getPlayingOffset() == sf::Time::Zero); + CHECK(soundCopy.getStatus() == sf::Sound::Status::Stopped); + } + + SECTION("Assignment") + { + const sf::SoundBuffer emptySoundBuffer; + sf::Sound soundCopy(emptySoundBuffer); + soundCopy = sound; + CHECK(&soundCopy.getBuffer() == &soundBuffer); + CHECK(!soundCopy.getLoop()); + CHECK(soundCopy.getPlayingOffset() == sf::Time::Zero); + CHECK(soundCopy.getStatus() == sf::Sound::Status::Stopped); + } + } + + SECTION("Set/get buffer") + { + const sf::SoundBuffer otherSoundBuffer; + sf::Sound sound(soundBuffer); + sound.setBuffer(otherSoundBuffer); + CHECK(&sound.getBuffer() == &otherSoundBuffer); + } + + SECTION("Set/get loop") + { + sf::Sound sound(soundBuffer); + sound.setLoop(true); + CHECK(sound.getLoop()); + } + + SECTION("Set/get playing offset") + { + sf::Sound sound(soundBuffer); + sound.setPlayingOffset(sf::seconds(10)); + CHECK(sound.getPlayingOffset() == sf::seconds(10)); + } +} diff --git a/test/Audio/SoundBuffer.test.cpp b/test/Audio/SoundBuffer.test.cpp index c62ea06db..a907f2470 100644 --- a/test/Audio/SoundBuffer.test.cpp +++ b/test/Audio/SoundBuffer.test.cpp @@ -1,10 +1,147 @@ #include +// Other 1st party headers +#include + +#include + +#include +#include +#include #include -static_assert(std::is_copy_constructible_v); -static_assert(std::is_copy_assignable_v); -static_assert(std::is_move_constructible_v); -static_assert(!std::is_nothrow_move_constructible_v); -static_assert(std::is_move_assignable_v); -static_assert(!std::is_nothrow_move_assignable_v); +TEST_CASE("[Audio] sf::SoundBuffer", runAudioDeviceTests()) +{ + SECTION("Type traits") + { + STATIC_CHECK(std::is_copy_constructible_v); + STATIC_CHECK(std::is_copy_assignable_v); + STATIC_CHECK(std::is_move_constructible_v); + STATIC_CHECK(!std::is_nothrow_move_constructible_v); + STATIC_CHECK(std::is_move_assignable_v); + STATIC_CHECK(!std::is_nothrow_move_assignable_v); + } + + SECTION("Construction") + { + const sf::SoundBuffer soundBuffer; + CHECK(soundBuffer.getSamples() == nullptr); + CHECK(soundBuffer.getSampleCount() == 0); + CHECK(soundBuffer.getSampleRate() == 44100); + CHECK(soundBuffer.getChannelCount() == 1); + CHECK(soundBuffer.getDuration() == sf::Time::Zero); + } + + SECTION("Copy semantics") + { + sf::SoundBuffer soundBuffer; + REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac")); + + SECTION("Construction") + { + const sf::SoundBuffer soundBufferCopy(soundBuffer); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(soundBufferCopy.getSamples() != nullptr); + CHECK(soundBufferCopy.getSampleCount() == 87798); + CHECK(soundBufferCopy.getSampleRate() == 44100); + CHECK(soundBufferCopy.getChannelCount() == 1); + CHECK(soundBufferCopy.getDuration() == sf::microseconds(1990884)); + } + + SECTION("Assignment") + { + sf::SoundBuffer soundBufferCopy; + soundBufferCopy = soundBuffer; + CHECK(soundBufferCopy.getSamples() != nullptr); + CHECK(soundBufferCopy.getSampleCount() == 87798); + CHECK(soundBufferCopy.getSampleRate() == 44100); + CHECK(soundBufferCopy.getChannelCount() == 1); + CHECK(soundBufferCopy.getDuration() == sf::microseconds(1990884)); + } + } + + SECTION("loadFromFile()") + { + sf::SoundBuffer soundBuffer; + + SECTION("Invalid filename") + { + CHECK(!soundBuffer.loadFromFile("does/not/exist.wav")); + } + + SECTION("Valid file") + { + REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac")); + CHECK(soundBuffer.getSamples() != nullptr); + CHECK(soundBuffer.getSampleCount() == 87798); + CHECK(soundBuffer.getSampleRate() == 44100); + CHECK(soundBuffer.getChannelCount() == 1); + CHECK(soundBuffer.getDuration() == sf::microseconds(1990884)); + } + } + + SECTION("loadFromMemory()") + { + sf::SoundBuffer soundBuffer; + + SECTION("Invalid memory") + { + CHECK(!soundBuffer.loadFromMemory(nullptr, 0)); + constexpr std::array memory{}; + CHECK(!soundBuffer.loadFromMemory(memory.data(), memory.size())); + } + + SECTION("Valid memory") + { + const auto memory = loadIntoMemory("Audio/ding.flac"); + REQUIRE(soundBuffer.loadFromMemory(memory.data(), memory.size())); + CHECK(soundBuffer.getSamples() != nullptr); + CHECK(soundBuffer.getSampleCount() == 87798); + CHECK(soundBuffer.getSampleRate() == 44100); + CHECK(soundBuffer.getChannelCount() == 1); + CHECK(soundBuffer.getDuration() == sf::microseconds(1990884)); + } + } + + SECTION("loadFromStream()") + { + sf::FileInputStream stream; + sf::SoundBuffer soundBuffer; + + SECTION("Invalid stream") + { + CHECK(!soundBuffer.loadFromStream(stream)); + } + + SECTION("Valid stream") + { + REQUIRE(stream.open("Audio/ding.flac")); + REQUIRE(soundBuffer.loadFromStream(stream)); + CHECK(soundBuffer.getSamples() != nullptr); + CHECK(soundBuffer.getSampleCount() == 87798); + CHECK(soundBuffer.getSampleRate() == 44100); + CHECK(soundBuffer.getChannelCount() == 1); + CHECK(soundBuffer.getDuration() == sf::microseconds(1990884)); + } + } + + SECTION("saveToFile()") + { + const auto filename = std::filesystem::temp_directory_path() / "ding.flac"; + + { + sf::SoundBuffer soundBuffer; + REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac")); + REQUIRE(soundBuffer.saveToFile(filename)); + } + + sf::SoundBuffer soundBuffer; + REQUIRE(soundBuffer.loadFromFile(filename)); + CHECK(soundBuffer.getSamples() != nullptr); + CHECK(soundBuffer.getSampleCount() == 87798); + CHECK(soundBuffer.getSampleRate() == 44100); + CHECK(soundBuffer.getChannelCount() == 1); + CHECK(soundBuffer.getDuration() == sf::microseconds(1990884)); + + CHECK(std::filesystem::remove(filename)); + } +} diff --git a/test/Audio/SoundSource.test.cpp b/test/Audio/SoundSource.test.cpp index 4804e6781..8fafb31dc 100644 --- a/test/Audio/SoundSource.test.cpp +++ b/test/Audio/SoundSource.test.cpp @@ -1,10 +1,134 @@ #include +#include + +#include +#include #include -static_assert(!std::is_constructible_v); -static_assert(!std::is_copy_constructible_v); -static_assert(std::is_copy_assignable_v); -static_assert(!std::is_move_constructible_v); -static_assert(std::is_move_assignable_v); -static_assert(!std::is_nothrow_move_assignable_v); +namespace +{ +class SoundSource : public sf::SoundSource +{ + void play() override + { + } + + void pause() override + { + } + + void stop() override + { + } + + void* getSound() const override + { + return {}; + } + +public: + Status getStatus() const override + { + return {}; + } +}; +} // namespace + +TEST_CASE("[Audio] sf::SoundSource", runAudioDeviceTests()) +{ + SECTION("Type traits") + { + STATIC_CHECK(!std::is_constructible_v); + STATIC_CHECK(!std::is_copy_constructible_v); + STATIC_CHECK(std::is_copy_assignable_v); + STATIC_CHECK(!std::is_move_constructible_v); + STATIC_CHECK(std::is_move_assignable_v); + STATIC_CHECK(!std::is_nothrow_move_assignable_v); + STATIC_CHECK(std::has_virtual_destructor_v); + } + + SECTION("Construction") + { + const SoundSource soundSource; + CHECK(soundSource.getPitch() == 0); + CHECK(soundSource.getVolume() == 0); + CHECK(soundSource.getPosition() == sf::Vector3f()); + CHECK(!soundSource.isRelativeToListener()); + CHECK(soundSource.getMinDistance() == 0); + CHECK(soundSource.getAttenuation() == 0); + CHECK(soundSource.getStatus() == sf::SoundSource::Status::Stopped); + } + + SECTION("Copy semantics") + { + const SoundSource soundSource; + + SECTION("Construction") + { + const SoundSource soundSourceCopy(soundSource); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(soundSourceCopy.getPitch() == 0); + CHECK(soundSourceCopy.getVolume() == 0); + CHECK(soundSourceCopy.getPosition() == sf::Vector3f()); + CHECK(!soundSourceCopy.isRelativeToListener()); + CHECK(soundSourceCopy.getMinDistance() == 0); + CHECK(soundSourceCopy.getAttenuation() == 0); + CHECK(soundSourceCopy.getStatus() == sf::SoundSource::Status::Stopped); + } + + SECTION("Assignment") + { + SoundSource soundSourceCopy; + soundSourceCopy = soundSource; + CHECK(soundSourceCopy.getPitch() == 0); + CHECK(soundSourceCopy.getVolume() == 0); + CHECK(soundSourceCopy.getPosition() == sf::Vector3f()); + CHECK(!soundSourceCopy.isRelativeToListener()); + CHECK(soundSourceCopy.getMinDistance() == 0); + CHECK(soundSourceCopy.getAttenuation() == 0); + CHECK(soundSourceCopy.getStatus() == sf::SoundSource::Status::Stopped); + } + } + + SECTION("Set/get pitch") + { + SoundSource soundSource; + soundSource.setPitch(42); + CHECK(soundSource.getPitch() == 0); + } + + SECTION("Set/get volume") + { + SoundSource soundSource; + soundSource.setVolume(0.5f); + CHECK(soundSource.getVolume() == 0); + } + + SECTION("Set/get position") + { + SoundSource soundSource; + soundSource.setPosition({1, 2, 3}); + CHECK(soundSource.getPosition() == sf::Vector3f()); + } + + SECTION("Set/get relative to listener") + { + SoundSource soundSource; + soundSource.setRelativeToListener(true); + CHECK(!soundSource.isRelativeToListener()); + } + + SECTION("Set/get min distance") + { + SoundSource soundSource; + soundSource.setMinDistance(12.34f); + CHECK(soundSource.getMinDistance() == 0); + } + + SECTION("Set/get attenuation") + { + SoundSource soundSource; + soundSource.setAttenuation(10); + CHECK(soundSource.getAttenuation() == 0); + } +} diff --git a/test/Audio/SoundStream.test.cpp b/test/Audio/SoundStream.test.cpp index aca5e5525..42d04897a 100644 --- a/test/Audio/SoundStream.test.cpp +++ b/test/Audio/SoundStream.test.cpp @@ -2,9 +2,26 @@ #include +#include +#include #include -TEST_CASE("[Audio] sf::SoundStream") +namespace +{ +class SoundStream : public sf::SoundStream +{ + [[nodiscard]] bool onGetData(Chunk& /* data */) override + { + return true; + } + + void onSeek(sf::Time /* timeOffset */) override + { + } +}; +} // namespace + +TEST_CASE("[Audio] sf::SoundStream", runAudioDeviceTests()) { SECTION("Type traits") { @@ -13,6 +30,7 @@ TEST_CASE("[Audio] sf::SoundStream") STATIC_CHECK(!std::is_copy_assignable_v); STATIC_CHECK(!std::is_nothrow_move_constructible_v); STATIC_CHECK(!std::is_nothrow_move_assignable_v); + STATIC_CHECK(std::has_virtual_destructor_v); } SECTION("Chunk") @@ -21,4 +39,28 @@ TEST_CASE("[Audio] sf::SoundStream") CHECK(chunk.samples == nullptr); CHECK(chunk.sampleCount == 0); } + + SECTION("Construction") + { + const SoundStream soundStream; + CHECK(soundStream.getChannelCount() == 0); + CHECK(soundStream.getSampleRate() == 0); + CHECK(soundStream.getStatus() == sf::SoundStream::Status::Stopped); + CHECK(soundStream.getPlayingOffset() == sf::Time::Zero); + CHECK(!soundStream.getLoop()); + } + + SECTION("Set/get playing offset") + { + SoundStream soundStream; + soundStream.setPlayingOffset(sf::milliseconds(100)); + CHECK(soundStream.getPlayingOffset() == sf::milliseconds(0)); + } + + SECTION("Set/get loop") + { + SoundStream soundStream; + soundStream.setLoop(true); + CHECK(soundStream.getLoop()); + } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c2986dde1..d95a61358 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,7 +7,7 @@ set(CATCH_CONFIG_FAST_COMPILE ON CACHE BOOL "") set(CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ON CACHE BOOL "") FetchContent_Declare(Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.5.4 + GIT_TAG v3.6.0 GIT_SHALLOW ON) FetchContent_MakeAvailable(Catch2) include(Catch) @@ -28,6 +28,8 @@ add_library(sfml-test-main STATIC TestUtilities/WindowUtil.cpp TestUtilities/GraphicsUtil.hpp TestUtilities/GraphicsUtil.cpp + TestUtilities/AudioUtil.hpp + TestUtilities/AudioUtil.cpp ) target_include_directories(sfml-test-main PUBLIC TestUtilities) target_link_libraries(sfml-test-main PUBLIC SFML::System Catch2::Catch2WithMain) @@ -43,6 +45,11 @@ if(SFML_RUN_DISPLAY_TESTS) target_compile_definitions(sfml-test-main PRIVATE SFML_RUN_DISPLAY_TESTS) endif() +sfml_set_option(SFML_RUN_AUDIO_DEVICE_TESTS ON BOOL "TRUE to run tests that require an audio device, FALSE to ignore it") +if(SFML_RUN_AUDIO_DEVICE_TESTS) + target_compile_definitions(sfml-test-main PRIVATE SFML_RUN_AUDIO_DEVICE_TESTS) +endif() + set(SYSTEM_SRC System/Angle.test.cpp System/Clock.test.cpp @@ -145,6 +152,41 @@ set(AUDIO_SRC ) sfml_add_test(test-sfml-audio "${AUDIO_SRC}" SFML::Audio) +if(SFML_OS_ANDROID AND DEFINED ENV{LIBCXX_SHARED_SO}) + # Because we can only write to the tmp directory on the Android virtual device we will need to build our directory tree under it + set(TARGET_DIR "/data/local/tmp/$") + + # Generate script that copies necessary files over to the Android virtual device + file(GENERATE OUTPUT "${PROJECT_BINARY_DIR}/prepare-android-files.sh" CONTENT + "#!/bin/bash\n\ + adb shell \"mkdir -p ${TARGET_DIR}\"\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ ${TARGET_DIR}\n\ + adb push $ENV{LIBCXX_SHARED_SO} ${TARGET_DIR}\n\ + adb push ${CMAKE_CURRENT_LIST_DIR} ${TARGET_DIR}\n\ + adb shell \"chmod -R 775 ${TARGET_DIR} && ls -la ${TARGET_DIR}\"\n" + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) + + # Add the target to invoke the file copy script + add_custom_target(prepare-android-files COMMAND "${PROJECT_BINARY_DIR}/prepare-android-files.sh") + + # Generate proxy script that translates CTest commands into adb shell commands + file(GENERATE OUTPUT "${PROJECT_BINARY_DIR}/run-in-adb-shell.sh" CONTENT + "#!/bin/bash\n\ + adb shell \"cd ${TARGET_DIR}/test; LD_LIBRARY_PATH=${TARGET_DIR} /data/local/tmp/$1 \\\"$2\\\" \\\"$3\\\" \\\"$4\\\" \\\"$5\\\" \\\"$6\\\" \\\"$7\\\" \\\"$8\\\" \\\"$9\\\"\"\n" + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +endif() + if(SFML_ENABLE_COVERAGE AND SFML_OS_WINDOWS AND NOT SFML_COMPILER_GCC) # Try to find and use OpenCppCoverage for coverage reporting when building with MSVC find_program(OpenCppCoverage_BINARY "OpenCppCoverage.exe") diff --git a/test/Graphics/Transformable.test.cpp b/test/Graphics/Transformable.test.cpp index 9ed5e890e..b980fb282 100644 --- a/test/Graphics/Transformable.test.cpp +++ b/test/Graphics/Transformable.test.cpp @@ -34,11 +34,11 @@ TEST_CASE("[Graphics] sf::Transformable") CHECK(transformable.getPosition() == sf::Vector2f(3, 4)); transformable.setRotation(sf::degrees(3.14f)); - CHECK(transformable.getRotation() == sf::degrees(3.14f)); + CHECK(transformable.getRotation() == Approx(sf::degrees(3.14f))); transformable.setRotation(sf::degrees(540)); - CHECK(transformable.getRotation() == sf::degrees(180)); + CHECK(transformable.getRotation() == Approx(sf::degrees(180))); transformable.setRotation(sf::degrees(-72)); - CHECK(transformable.getRotation() == sf::degrees(288)); + CHECK(transformable.getRotation() == Approx(sf::degrees(288))); transformable.setScale({5, 6}); CHECK(transformable.getScale() == sf::Vector2f(5, 6)); @@ -102,15 +102,15 @@ TEST_CASE("[Graphics] sf::Transformable") sf::Transformable transformable; CHECK(transformable.getRotation() == sf::Angle::Zero); transformable.rotate(sf::degrees(15)); - CHECK(transformable.getRotation() == sf::degrees(15)); + CHECK(transformable.getRotation() == Approx(sf::degrees(15))); transformable.rotate(sf::degrees(360)); - CHECK(transformable.getRotation() == sf::degrees(15)); + CHECK(transformable.getRotation() == Approx(sf::degrees(15))); transformable.rotate(sf::degrees(-25)); - CHECK(transformable.getRotation() == sf::degrees(350)); + CHECK(transformable.getRotation() == Approx(sf::degrees(350))); transformable.rotate(sf::degrees(-720)); - CHECK(transformable.getRotation() == sf::degrees(350)); + CHECK(transformable.getRotation() == Approx(sf::degrees(350))); transformable.rotate(sf::degrees(-370)); - CHECK(transformable.getRotation() == sf::degrees(340)); + CHECK(transformable.getRotation() == Approx(sf::degrees(340))); } SECTION("scale()") diff --git a/test/Graphics/View.test.cpp b/test/Graphics/View.test.cpp index f9b3497d9..3cb829311 100644 --- a/test/Graphics/View.test.cpp +++ b/test/Graphics/View.test.cpp @@ -76,13 +76,13 @@ TEST_CASE("[Graphics] sf::View") { sf::View view; view.setRotation(sf::degrees(-345)); - CHECK(view.getRotation() == sf::degrees(15)); + CHECK(view.getRotation() == Approx(sf::degrees(15))); CHECK(view.getTransform() == Approx(sf::Transform(0.00193185f, 0.000517638f, -1.22474f, 0.000517638f, -0.00193185f, 0.707107f, 0, 0, 1))); CHECK(view.getInverseTransform() == Approx(sf::Transform(482.963f, 129.41f, 500, 129.41f, -482.963f, 500, 0, 0, 1))); view.setRotation(sf::degrees(400)); - CHECK(view.getRotation() == sf::degrees(40)); + CHECK(view.getRotation() == Approx(sf::degrees(40))); CHECK(view.getTransform() == Approx(sf::Transform(0.00153209f, 0.00128558f, -1.40883f, 0.00128558f, -0.00153209f, 0.123257f, 0, 0, 1))); CHECK(view.getInverseTransform() == @@ -140,7 +140,7 @@ TEST_CASE("[Graphics] sf::View") sf::View view; view.setRotation(sf::degrees(45)); view.rotate(sf::degrees(-15)); - CHECK(view.getRotation() == sf::degrees(30)); + CHECK(view.getRotation() == Approx(sf::degrees(30))); CHECK(view.getTransform() == Approx(sf::Transform(0.00173205f, 0.001f, -1.36603f, 0.001f, -0.00173205f, 0.366025f, 0, 0, 1))); CHECK(view.getInverseTransform() == Approx(sf::Transform(433.013f, 250, 500, 250, -433.013f, 500, 0, 0, 1))); diff --git a/test/System/Angle.test.cpp b/test/System/Angle.test.cpp index 4934a27f8..a438ea5f9 100644 --- a/test/System/Angle.test.cpp +++ b/test/System/Angle.test.cpp @@ -25,33 +25,33 @@ TEST_CASE("[System] sf::Angle") SECTION("wrapSigned()") { STATIC_CHECK(sf::Angle::Zero.wrapSigned() == sf::Angle::Zero); - STATIC_CHECK(sf::degrees(0).wrapSigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(1).wrapSigned() == sf::degrees(1)); - STATIC_CHECK(sf::degrees(-1).wrapSigned() == sf::degrees(-1)); - STATIC_CHECK(sf::degrees(90).wrapSigned() == sf::degrees(90)); - STATIC_CHECK(sf::degrees(-90).wrapSigned() == sf::degrees(-90)); - STATIC_CHECK(sf::degrees(180).wrapSigned() == sf::degrees(-180)); - STATIC_CHECK(sf::degrees(-180).wrapSigned() == sf::degrees(-180)); - STATIC_CHECK(sf::degrees(360).wrapSigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(-360).wrapSigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(720).wrapSigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(-720).wrapSigned() == sf::degrees(0)); + CHECK(sf::degrees(0).wrapSigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(1).wrapSigned() == Approx(sf::degrees(1))); + CHECK(sf::degrees(-1).wrapSigned() == Approx(sf::degrees(-1))); + CHECK(sf::degrees(90).wrapSigned() == Approx(sf::degrees(90))); + CHECK(sf::degrees(-90).wrapSigned() == Approx(sf::degrees(-90))); + CHECK(sf::degrees(180).wrapSigned() == Approx(sf::degrees(-180))); + CHECK(sf::degrees(-180).wrapSigned() == Approx(sf::degrees(-180))); + CHECK(sf::degrees(360).wrapSigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(-360).wrapSigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(720).wrapSigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(-720).wrapSigned() == Approx(sf::degrees(0))); } SECTION("wrapUnsigned()") { STATIC_CHECK(sf::Angle::Zero.wrapUnsigned() == sf::Angle::Zero); - STATIC_CHECK(sf::degrees(0).wrapUnsigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(1).wrapUnsigned() == sf::degrees(1)); - STATIC_CHECK(sf::degrees(-1).wrapUnsigned() == sf::degrees(359)); - STATIC_CHECK(sf::degrees(90).wrapUnsigned() == sf::degrees(90)); - STATIC_CHECK(sf::degrees(-90).wrapUnsigned() == sf::degrees(270)); - STATIC_CHECK(sf::degrees(180).wrapUnsigned() == sf::degrees(180)); - STATIC_CHECK(sf::degrees(-180).wrapUnsigned() == sf::degrees(180)); - STATIC_CHECK(sf::degrees(360).wrapUnsigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(-360).wrapUnsigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(720).wrapUnsigned() == sf::degrees(0)); - STATIC_CHECK(sf::degrees(-720).wrapUnsigned() == sf::degrees(0)); + CHECK(sf::degrees(0).wrapUnsigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(1).wrapUnsigned() == Approx(sf::degrees(1))); + CHECK(sf::degrees(-1).wrapUnsigned() == Approx(sf::degrees(359))); + CHECK(sf::degrees(90).wrapUnsigned() == Approx(sf::degrees(90))); + CHECK(sf::degrees(-90).wrapUnsigned() == Approx(sf::degrees(270))); + CHECK(sf::degrees(180).wrapUnsigned() == Approx(sf::degrees(180))); + CHECK(sf::degrees(-180).wrapUnsigned() == Approx(sf::degrees(180))); + CHECK(sf::degrees(360).wrapUnsigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(-360).wrapUnsigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(720).wrapUnsigned() == Approx(sf::degrees(0))); + CHECK(sf::degrees(-720).wrapUnsigned() == Approx(sf::degrees(0))); } SECTION("degrees()") @@ -185,7 +185,7 @@ TEST_CASE("[System] sf::Angle") { sf::Angle angle = sf::degrees(-15); angle += sf::degrees(15); - CHECK(angle == sf::degrees(0)); + CHECK(angle == Approx(sf::degrees(0))); angle += sf::radians(10); CHECK(angle == sf::radians(10)); } @@ -202,7 +202,7 @@ TEST_CASE("[System] sf::Angle") { sf::Angle angle = sf::degrees(15); angle -= sf::degrees(15); - CHECK(angle == sf::degrees(0)); + CHECK(angle == Approx(sf::degrees(0))); angle -= sf::radians(10); CHECK(angle == sf::radians(-10)); } @@ -210,19 +210,19 @@ TEST_CASE("[System] sf::Angle") SECTION("operator*") { STATIC_CHECK(sf::radians(0) * 10 == sf::Angle::Zero); - STATIC_CHECK(sf::degrees(10) * 2.5f == sf::degrees(25)); - STATIC_CHECK(sf::degrees(100) * 10.0f == sf::degrees(1000)); + CHECK(sf::degrees(10) * 2.5f == Approx(sf::degrees(25))); + CHECK(sf::degrees(100) * 10.0f == Approx(sf::degrees(1000))); STATIC_CHECK(10 * sf::radians(0) == sf::Angle::Zero); - STATIC_CHECK(2.5f * sf::degrees(10) == sf::degrees(25)); - STATIC_CHECK(10.0f * sf::degrees(100) == sf::degrees(1000)); + CHECK(2.5f * sf::degrees(10) == Approx(sf::degrees(25))); + CHECK(10.0f * sf::degrees(100) == Approx(sf::degrees(1000))); } SECTION("operator*=") { sf::Angle angle = sf::degrees(1); angle *= 10; - CHECK(angle == sf::degrees(10)); + CHECK(angle == Approx(sf::degrees(10))); } SECTION("operator/") @@ -240,24 +240,24 @@ TEST_CASE("[System] sf::Angle") { sf::Angle angle = sf::degrees(60); angle /= 5; - CHECK(angle == sf::degrees(12)); + CHECK(angle == Approx(sf::degrees(12))); } SECTION("operator%") { STATIC_CHECK(sf::Angle::Zero % sf::radians(0.5f) == sf::Angle::Zero); STATIC_CHECK(sf::radians(10) % sf::radians(1) == sf::radians(0)); - STATIC_CHECK(sf::degrees(90) % sf::degrees(30) == sf::degrees(0)); - STATIC_CHECK(sf::degrees(90) % sf::degrees(40) == sf::degrees(10)); - STATIC_CHECK(sf::degrees(-90) % sf::degrees(30) == sf::degrees(0)); - STATIC_CHECK(sf::degrees(-90) % sf::degrees(40) == sf::degrees(30)); + CHECK(sf::degrees(90) % sf::degrees(30) == Approx(sf::degrees(0))); + CHECK(sf::degrees(90) % sf::degrees(40) == Approx(sf::degrees(10))); + CHECK(sf::degrees(-90) % sf::degrees(30) == Approx(sf::degrees(0))); + CHECK(sf::degrees(-90) % sf::degrees(40) == Approx(sf::degrees(30))); } SECTION("operator%=") { sf::Angle angle = sf::degrees(59); angle %= sf::degrees(10); - CHECK(angle == sf::degrees(9)); + CHECK(angle == Approx(sf::degrees(9))); } SECTION("operator _deg") diff --git a/test/System/MemoryInputStream.test.cpp b/test/System/MemoryInputStream.test.cpp index 06a12fddd..5b5845d65 100644 --- a/test/System/MemoryInputStream.test.cpp +++ b/test/System/MemoryInputStream.test.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -32,9 +33,9 @@ TEST_CASE("[System] sf::MemoryInputStream") sf::MemoryInputStream mis; mis.open(memoryContents.data(), sizeof(char) * memoryContents.size()); - char buffer[32]; - CHECK(mis.read(buffer, 5) == 5); - CHECK(std::string_view(buffer, 5) == std::string_view(memoryContents.data(), 5)); + std::array buffer{}; + CHECK(mis.read(buffer.data(), 5) == 5); + CHECK(std::string_view(buffer.data(), 5) == std::string_view(memoryContents.data(), 5)); CHECK(mis.seek(10) == 10); CHECK(mis.tell() == 10); CHECK(mis.getSize() == 11); diff --git a/test/TestUtilities/AudioUtil.cpp b/test/TestUtilities/AudioUtil.cpp new file mode 100644 index 000000000..30b4f50ca --- /dev/null +++ b/test/TestUtilities/AudioUtil.cpp @@ -0,0 +1,12 @@ +#include + +std::string runAudioDeviceTests() +{ +#ifdef SFML_RUN_AUDIO_DEVICE_TESTS + return ""; +#else + // https://github.com/catchorg/Catch2/blob/devel/docs/test-cases-and-sections.md#special-tags + // This tag tells Catch2 to not run a given TEST_CASE + return "[.audio_device]"; +#endif +} diff --git a/test/TestUtilities/AudioUtil.hpp b/test/TestUtilities/AudioUtil.hpp new file mode 100644 index 000000000..c4d759c39 --- /dev/null +++ b/test/TestUtilities/AudioUtil.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +[[nodiscard]] std::string runAudioDeviceTests(); diff --git a/test/TestUtilities/SystemUtil.cpp b/test/TestUtilities/SystemUtil.cpp index 95f5a78c8..c731012ea 100644 --- a/test/TestUtilities/SystemUtil.cpp +++ b/test/TestUtilities/SystemUtil.cpp @@ -77,7 +77,7 @@ bool operator==(const sf::Vector3f& lhs, const Approx& rhs) bool operator==(const sf::Angle& lhs, const Approx& rhs) { - return lhs.asDegrees() == Approx(rhs.value.asDegrees()); + return lhs.asRadians() == Approx(rhs.value.asRadians()); } std::vector loadIntoMemory(const std::filesystem::path& path) diff --git a/tools/pkg-config/sfml-all.pc.in b/tools/pkg-config/sfml-all.pc.in index 280ee4ecd..0da40135e 100644 --- a/tools/pkg-config/sfml-all.pc.in +++ b/tools/pkg-config/sfml-all.pc.in @@ -1,6 +1,6 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@ includedir=${prefix}/include Name: SFML-all diff --git a/tools/pkg-config/sfml-audio.pc.in b/tools/pkg-config/sfml-audio.pc.in index 459cb2cd4..f2faf6ebc 100644 --- a/tools/pkg-config/sfml-audio.pc.in +++ b/tools/pkg-config/sfml-audio.pc.in @@ -1,6 +1,6 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@ includedir=${prefix}/include Name: SFML-audio diff --git a/tools/pkg-config/sfml-graphics.pc.in b/tools/pkg-config/sfml-graphics.pc.in index 383ecb983..de796a514 100644 --- a/tools/pkg-config/sfml-graphics.pc.in +++ b/tools/pkg-config/sfml-graphics.pc.in @@ -1,6 +1,6 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@ includedir=${prefix}/include Name: SFML-graphics diff --git a/tools/pkg-config/sfml-network.pc.in b/tools/pkg-config/sfml-network.pc.in index d90e24b21..5a00d38a5 100644 --- a/tools/pkg-config/sfml-network.pc.in +++ b/tools/pkg-config/sfml-network.pc.in @@ -1,6 +1,6 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@ includedir=${prefix}/include Name: SFML-network diff --git a/tools/pkg-config/sfml-system.pc.in b/tools/pkg-config/sfml-system.pc.in index a353844f5..5cc76ef1b 100644 --- a/tools/pkg-config/sfml-system.pc.in +++ b/tools/pkg-config/sfml-system.pc.in @@ -1,6 +1,6 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@ includedir=${prefix}/include Name: SFML-system diff --git a/tools/pkg-config/sfml-window.pc.in b/tools/pkg-config/sfml-window.pc.in index d4e5b4206..2041ec50e 100644 --- a/tools/pkg-config/sfml-window.pc.in +++ b/tools/pkg-config/sfml-window.pc.in @@ -1,6 +1,6 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@ includedir=${prefix}/include Name: SFML-window